from pathlib import Path from flask import Flask, Response, render_template, abort, send_file, jsonify import markdown as md from dataclasses import dataclass from dataclasses_json import DataClassJsonMixin app = Flask(__name__) BUILDS_PATH = Path("../builds") ARTIFACTS_FOLDER = "artifacts" INFO_FILE = "info.json" @dataclass class ArtifactItem(DataClassJsonMixin): file: str description: str @dataclass class Artifact(DataClassJsonMixin): id: str date: str changelog: str artifacts: list[ArtifactItem] hash: str | None = None short_hash: str | None = None @dataclass class ProjectInfo(DataClassJsonMixin): name: str commit_url: str @dataclass class Project: id: str info: ProjectInfo def _get_artifact_paths(self) -> list[Path]: artifacts_path = BUILDS_PATH / self.id / ARTIFACTS_FOLDER if not artifacts_path.exists(): return [] return sorted( [p for p in artifacts_path.iterdir() if p.is_dir()], reverse=True, ) def get_artifacts(self) -> list[Artifact]: result = [] for artifact_folder in self._get_artifact_paths(): info_file_path = artifact_folder / INFO_FILE artifact = self._parse_artifact(info_file_path) if artifact: result.append(artifact) return result def get_lastest_artifact(self) -> Artifact | None: latest_artifact_folder = max(self._get_artifact_paths(), default=None) if not latest_artifact_folder: return None return self._parse_artifact(latest_artifact_folder / INFO_FILE) def get_artifact(self, artifact_id: str) -> Artifact | None: artifact_folder = BUILDS_PATH / self.id / ARTIFACTS_FOLDER / artifact_id if not artifact_folder.is_dir(): return None return self._parse_artifact(artifact_folder / INFO_FILE) def _parse_artifact(self, info_file_path: Path) -> Artifact | None: if not info_file_path.exists(): return None try: with info_file_path.open("r", encoding="utf-8") as f: artifact = Artifact.from_json(f.read()) artifact.date = artifact.date.strip() return artifact except: return None def get_projects() -> list[Project]: result = [] projects = [p for p in BUILDS_PATH.iterdir() if p.is_dir()] for project in projects: info_path = project / INFO_FILE if not info_path.exists(): continue try: *_, proj_id = project.parts with info_path.open("r", encoding="utf-8") as f: proj_info = ProjectInfo.from_json(f.read()) result.append(Project(proj_id, proj_info)) except: continue return result @app.get("/api/projects//artifacts/latest") def get_latest_artifact(project_id: str) -> Response: projects = get_projects() selected_project = next( (project for project in projects if project.id == project_id), None ) if not selected_project: return abort(404) artifact = selected_project.get_lastest_artifact() if not artifact: return abort(404) return jsonify(artifact) @app.get("/api/projects//artifacts/") def get_artifact_by_id(project_id: str, artifact_id: str) -> Response: projects = get_projects() selected_project = next( (project for project in projects if project.id == project_id), None ) if not selected_project: return abort(404) artifact = selected_project.get_artifact(artifact_id) if not artifact: return abort(404) return jsonify(artifact) @app.get("/projects///") def download_item(project_id: str, artifact_id: str, download_item: str) -> Response: file_path = ( BUILDS_PATH / project_id / ARTIFACTS_FOLDER / artifact_id / download_item ) if not file_path.exists(): return abort(404) return send_file(file_path) @app.get("/projects/") def display_project(project_id: str) -> Response | str: projects = get_projects() selected_project = next( (project for project in projects if project.id == project_id), None ) if not selected_project: return abort(404) info_path = BUILDS_PATH / selected_project.id / "info.md" readme = None try: if info_path.exists(): with info_path.open("r", encoding="utf-8") as f: readme = md.markdown(f.read()) except: readme = None artifacts = selected_project.get_artifacts() return render_template( "project_view.html", projects=projects, selected_project=selected_project, readme=readme, artifacts=artifacts, ) @app.get("/") def index() -> Response | str: projects = get_projects() return render_template("main.html", projects=projects) if __name__ == "__main__": app.run(host="0.0.0.0")