|  | @@ -1,13 +1,16 @@
 | 
	
		
			
				|  |  | -from flask import Flask, render_template, abort, send_file, jsonify
 | 
	
		
			
				|  |  | -import os
 | 
	
		
			
				|  |  | -import json
 | 
	
		
			
				|  |  | -from typing import List
 | 
	
		
			
				|  |  | +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
 | 
	
	
		
			
				|  | @@ -19,9 +22,9 @@ class Artifact(DataClassJsonMixin):
 | 
	
		
			
				|  |  |      id: str
 | 
	
		
			
				|  |  |      date: str
 | 
	
		
			
				|  |  |      changelog: str
 | 
	
		
			
				|  |  | -    artifacts: List[ArtifactItem]
 | 
	
		
			
				|  |  | -    hash: str = None
 | 
	
		
			
				|  |  | -    short_hash: str = None
 | 
	
		
			
				|  |  | +    artifacts: list[ArtifactItem]
 | 
	
		
			
				|  |  | +    hash: str | None = None
 | 
	
		
			
				|  |  | +    short_hash: str | None = None
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  @dataclass
 | 
	
	
		
			
				|  | @@ -30,50 +33,47 @@ class ProjectInfo(DataClassJsonMixin):
 | 
	
		
			
				|  |  |      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 []
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def get_artifacts(self) -> List[Artifact]:
 | 
	
		
			
				|  |  | -        result = []
 | 
	
		
			
				|  |  | -        artifacts_path = os.path.join("../builds", self.id, "artifacts")
 | 
	
		
			
				|  |  | -        artifact_folders = sorted([folder.path for folder in os.scandir(
 | 
	
		
			
				|  |  | -            artifacts_path) if folder.is_dir()], reverse=True)
 | 
	
		
			
				|  |  | +        return sorted(
 | 
	
		
			
				|  |  | +            [p for p in artifacts_path.iterdir() if p.is_dir()],
 | 
	
		
			
				|  |  | +            reverse=True,
 | 
	
		
			
				|  |  | +        )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        for artifact_folder in artifact_folders:
 | 
	
		
			
				|  |  | -            info_file_path = os.path.join(artifact_folder, "info.json")
 | 
	
		
			
				|  |  | +    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:
 | 
	
		
			
				|  |  | -        artifacts_path = os.path.join("../builds", self.id, "artifacts")
 | 
	
		
			
				|  |  | -        artifact_folders = sorted([folder.path for folder in os.scandir(
 | 
	
		
			
				|  |  | -            artifacts_path) if folder.is_dir()], reverse=True)
 | 
	
		
			
				|  |  | -        if not artifact_folders:
 | 
	
		
			
				|  |  | +    def get_lastest_artifact(self) -> Artifact | None:
 | 
	
		
			
				|  |  | +        latest_artifact_folder = max(self._get_artifact_paths(), default=None)
 | 
	
		
			
				|  |  | +        if not latest_artifact_folder:
 | 
	
		
			
				|  |  |              return None
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | -        latest_artifact_folder = max(artifact_folders)
 | 
	
		
			
				|  |  | -        info_file_path = os.path.join(latest_artifact_folder, "info.json")
 | 
	
		
			
				|  |  | -        return self._parse_artifact(info_file_path)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +        return self._parse_artifact(latest_artifact_folder / INFO_FILE)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def get_artifact(self, artifact_id) -> Artifact:
 | 
	
		
			
				|  |  | -        artifact_folder = os.path.join("../builds", self.id, "artifacts", artifact_id)
 | 
	
		
			
				|  |  | -        if not os.path.isdir(artifact_folder):
 | 
	
		
			
				|  |  | +    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
 | 
	
		
			
				|  |  | -        info_file_path = os.path.join(artifact_folder, "info.json")
 | 
	
		
			
				|  |  | -        return self._parse_artifact(info_file_path)
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | +        return self._parse_artifact(artifact_folder / INFO_FILE)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    def _parse_artifact(self, info_file_path):
 | 
	
		
			
				|  |  | -        if not os.path.exists(info_file_path):
 | 
	
		
			
				|  |  | +    def _parse_artifact(self, info_file_path: Path) -> Artifact | None:
 | 
	
		
			
				|  |  | +        if not info_file_path.exists():
 | 
	
		
			
				|  |  |              return None
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  | -            with open(info_file_path, "r", encoding="utf-8") as f:
 | 
	
		
			
				|  |  | +            with info_file_path.open("r", encoding="utf-8") as f:
 | 
	
		
			
				|  |  |                  artifact = Artifact.from_json(f.read())
 | 
	
		
			
				|  |  |                  artifact.date = artifact.date.strip()
 | 
	
		
			
				|  |  |                  return artifact
 | 
	
	
		
			
				|  | @@ -81,93 +81,102 @@ class Project:
 | 
	
		
			
				|  |  |              return None
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -def get_projects() -> List[Project]:
 | 
	
		
			
				|  |  | +def get_projects() -> list[Project]:
 | 
	
		
			
				|  |  |      result = []
 | 
	
		
			
				|  |  | -    projects = [f.path for f in os.scandir("../builds") if f.is_dir()]
 | 
	
		
			
				|  |  | +    projects = [p for p in BUILDS_PATH.iterdir() if p.is_dir()]
 | 
	
		
			
				|  |  |      for project in projects:
 | 
	
		
			
				|  |  | -        info_path = os.path.join(project, "info.json")
 | 
	
		
			
				|  |  | -        if not os.path.exists(info_path):
 | 
	
		
			
				|  |  | +        info_path = project / INFO_FILE
 | 
	
		
			
				|  |  | +        if not info_path.exists():
 | 
	
		
			
				|  |  |              continue
 | 
	
		
			
				|  |  |          try:
 | 
	
		
			
				|  |  | -            proj = Project()
 | 
	
		
			
				|  |  | -            _, proj.id = os.path.split(project)
 | 
	
		
			
				|  |  | -            with open(info_path, "r", encoding="utf-8") as f:
 | 
	
		
			
				|  |  | -                proj.info = ProjectInfo.from_json(f.read())
 | 
	
		
			
				|  |  | -            result.append(proj)
 | 
	
		
			
				|  |  | +            *_, 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.route("/api/projects/<string:project_id>/artifacts/latest")
 | 
	
		
			
				|  |  | -def get_latest_artifact(project_id):
 | 
	
		
			
				|  |  | +@app.get("/api/projects/<string:project_id>/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)
 | 
	
		
			
				|  |  | +        (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.route("/api/projects/<string:project_id>/artifacts/<string:artifact_id>")
 | 
	
		
			
				|  |  | -def get_artifact_by_id(project_id, artifact_id):
 | 
	
		
			
				|  |  | +@app.get("/api/projects/<string:project_id>/artifacts/<string:artifact_id>")
 | 
	
		
			
				|  |  | +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)
 | 
	
		
			
				|  |  | +        (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.route("/projects/<string:project_id>/<string:artifact_id>/<string:download_item>")
 | 
	
		
			
				|  |  | -def download_item(project_id, artifact_id, download_item):
 | 
	
		
			
				|  |  | -    file_path = os.path.join("../builds", project_id,
 | 
	
		
			
				|  |  | -                             "artifacts", artifact_id, download_item)
 | 
	
		
			
				|  |  | -    if not os.path.exists(file_path):
 | 
	
		
			
				|  |  | +@app.get("/projects/<string:project_id>/<string:artifact_id>/<string:download_item>")
 | 
	
		
			
				|  |  | +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.route("/projects/<string:project_id>")
 | 
	
		
			
				|  |  | -def display_project(project_id):
 | 
	
		
			
				|  |  | +@app.get("/projects/<string:project_id>")
 | 
	
		
			
				|  |  | +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)
 | 
	
		
			
				|  |  | +        (project for project in projects if project.id == project_id), None
 | 
	
		
			
				|  |  | +    )
 | 
	
		
			
				|  |  |      if not selected_project:
 | 
	
		
			
				|  |  |          return abort(404)
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -    info_path = os.path.join("../builds", selected_project.id, "info.md")
 | 
	
		
			
				|  |  | +    info_path = BUILDS_PATH / selected_project.id / "info.md"
 | 
	
		
			
				|  |  |      readme = None
 | 
	
		
			
				|  |  |      try:
 | 
	
		
			
				|  |  | -        if os.path.exists(info_path):
 | 
	
		
			
				|  |  | -            with open(info_path, "r", encoding="utf-8") as f:
 | 
	
		
			
				|  |  | +        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)
 | 
	
		
			
				|  |  | +    return render_template(
 | 
	
		
			
				|  |  | +        "project_view.html",
 | 
	
		
			
				|  |  | +        projects=projects,
 | 
	
		
			
				|  |  | +        selected_project=selected_project,
 | 
	
		
			
				|  |  | +        readme=readme,
 | 
	
		
			
				|  |  | +        artifacts=artifacts,
 | 
	
		
			
				|  |  | +    )
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -@app.route("/")
 | 
	
		
			
				|  |  | -def index():
 | 
	
		
			
				|  |  | +@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')
 | 
	
		
			
				|  |  | +    app.run(host="0.0.0.0")
 |