app.py 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. from pathlib import Path
  2. from flask import Flask, Response, render_template, abort, send_file, jsonify
  3. import markdown as md
  4. from dataclasses import dataclass
  5. from dataclasses_json import DataClassJsonMixin
  6. app = Flask(__name__)
  7. BUILDS_PATH = Path("../builds")
  8. ARTIFACTS_FOLDER = "artifacts"
  9. INFO_FILE = "info.json"
  10. @dataclass
  11. class ArtifactItem(DataClassJsonMixin):
  12. file: str
  13. description: str
  14. @dataclass
  15. class Artifact(DataClassJsonMixin):
  16. id: str
  17. date: str
  18. changelog: str
  19. artifacts: list[ArtifactItem]
  20. hash: str | None = None
  21. short_hash: str | None = None
  22. @dataclass
  23. class ProjectInfo(DataClassJsonMixin):
  24. name: str
  25. commit_url: str
  26. @dataclass
  27. class Project:
  28. id: str
  29. info: ProjectInfo
  30. def _get_artifact_paths(self) -> list[Path]:
  31. artifacts_path = BUILDS_PATH / self.id / ARTIFACTS_FOLDER
  32. if not artifacts_path.exists():
  33. return []
  34. return sorted(
  35. [p for p in artifacts_path.iterdir() if p.is_dir()],
  36. reverse=True,
  37. )
  38. def get_artifacts(self) -> list[Artifact]:
  39. result = []
  40. for artifact_folder in self._get_artifact_paths():
  41. info_file_path = artifact_folder / INFO_FILE
  42. artifact = self._parse_artifact(info_file_path)
  43. if artifact:
  44. result.append(artifact)
  45. return result
  46. def get_lastest_artifact(self) -> Artifact | None:
  47. latest_artifact_folder = max(self._get_artifact_paths(), default=None)
  48. if not latest_artifact_folder:
  49. return None
  50. return self._parse_artifact(latest_artifact_folder / INFO_FILE)
  51. def get_artifact(self, artifact_id: str) -> Artifact | None:
  52. artifact_folder = BUILDS_PATH / self.id / ARTIFACTS_FOLDER / artifact_id
  53. if not artifact_folder.is_dir():
  54. return None
  55. return self._parse_artifact(artifact_folder / INFO_FILE)
  56. def _parse_artifact(self, info_file_path: Path) -> Artifact | None:
  57. if not info_file_path.exists():
  58. return None
  59. try:
  60. with info_file_path.open("r", encoding="utf-8") as f:
  61. artifact = Artifact.from_json(f.read())
  62. artifact.date = artifact.date.strip()
  63. return artifact
  64. except:
  65. return None
  66. def get_projects() -> list[Project]:
  67. result = []
  68. projects = [p for p in BUILDS_PATH.iterdir() if p.is_dir()]
  69. for project in projects:
  70. info_path = project / INFO_FILE
  71. if not info_path.exists():
  72. continue
  73. try:
  74. *_, proj_id = project.parts
  75. with info_path.open("r", encoding="utf-8") as f:
  76. proj_info = ProjectInfo.from_json(f.read())
  77. result.append(Project(proj_id, proj_info))
  78. except:
  79. continue
  80. return result
  81. @app.get("/api/projects/<string:project_id>/artifacts/latest")
  82. def get_latest_artifact(project_id: str) -> Response:
  83. projects = get_projects()
  84. selected_project = next(
  85. (project for project in projects if project.id == project_id), None
  86. )
  87. if not selected_project:
  88. return abort(404)
  89. artifact = selected_project.get_lastest_artifact()
  90. if not artifact:
  91. return abort(404)
  92. return jsonify(artifact)
  93. @app.get("/api/projects/<string:project_id>/artifacts/<string:artifact_id>")
  94. def get_artifact_by_id(project_id: str, artifact_id: str) -> Response:
  95. projects = get_projects()
  96. selected_project = next(
  97. (project for project in projects if project.id == project_id), None
  98. )
  99. if not selected_project:
  100. return abort(404)
  101. artifact = selected_project.get_artifact(artifact_id)
  102. if not artifact:
  103. return abort(404)
  104. return jsonify(artifact)
  105. @app.get("/projects/<string:project_id>/<string:artifact_id>/<string:download_item>")
  106. def download_item(project_id: str, artifact_id: str, download_item: str) -> Response:
  107. file_path = (
  108. BUILDS_PATH / project_id / ARTIFACTS_FOLDER / artifact_id / download_item
  109. )
  110. if not file_path.exists():
  111. return abort(404)
  112. return send_file(file_path)
  113. @app.get("/projects/<string:project_id>")
  114. def display_project(project_id: str) -> Response | str:
  115. projects = get_projects()
  116. selected_project = next(
  117. (project for project in projects if project.id == project_id), None
  118. )
  119. if not selected_project:
  120. return abort(404)
  121. info_path = BUILDS_PATH / selected_project.id / "info.md"
  122. readme = None
  123. try:
  124. if info_path.exists():
  125. with info_path.open("r", encoding="utf-8") as f:
  126. readme = md.markdown(f.read())
  127. except:
  128. readme = None
  129. artifacts = selected_project.get_artifacts()
  130. return render_template(
  131. "project_view.html",
  132. projects=projects,
  133. selected_project=selected_project,
  134. readme=readme,
  135. artifacts=artifacts,
  136. )
  137. @app.get("/")
  138. def index() -> Response | str:
  139. projects = get_projects()
  140. return render_template("main.html", projects=projects)
  141. if __name__ == "__main__":
  142. app.run(host="0.0.0.0")