builds.py 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. from typing import List, Optional
  2. from fastapi import (
  3. APIRouter,
  4. HTTPException,
  5. Query,
  6. Path,
  7. status,
  8. Depends,
  9. Request
  10. )
  11. from fastapi.responses import FileResponse, PlainTextResponse
  12. from schemas import (
  13. BuildRequest,
  14. BuildSubmitResponse,
  15. BuildOut,
  16. )
  17. from services.builds import get_builds_service, BuildsService
  18. from core.limiter import limiter
  19. router = APIRouter(prefix="/builds", tags=["builds"])
  20. @router.post(
  21. "",
  22. response_model=BuildSubmitResponse,
  23. status_code=status.HTTP_201_CREATED,
  24. responses={
  25. 400: {"description": "Invalid build configuration"},
  26. 404: {"description": "Vehicle, board, or version not found"},
  27. 429: {
  28. "description": "Rate limit exceeded",
  29. "content": {
  30. "application/json": {
  31. "example": {
  32. "detail": "Too many requests. Try again after some time."
  33. }
  34. }
  35. }
  36. }
  37. }
  38. )
  39. @limiter.limit("10/hour")
  40. async def create_build(
  41. build_request: BuildRequest,
  42. request: Request,
  43. service: BuildsService = Depends(get_builds_service)
  44. ):
  45. """
  46. Create a new build request.
  47. Args:
  48. build_request: Build configuration including vehicle, board, version,
  49. and selected features
  50. Returns:
  51. Simple response with build_id, URL, and status
  52. Raises:
  53. 400: Invalid build configuration
  54. 404: Vehicle, board, or version not found
  55. 429: Rate limit exceeded
  56. """
  57. try:
  58. return service.create_build(build_request)
  59. except ValueError as e:
  60. raise HTTPException(status_code=400, detail=str(e))
  61. except Exception as e:
  62. raise HTTPException(status_code=400, detail=str(e))
  63. @router.get("", response_model=List[BuildOut])
  64. async def list_builds(
  65. vehicle_id: Optional[str] = Query(
  66. None, description="Filter by vehicle ID"
  67. ),
  68. board_id: Optional[str] = Query(
  69. None, description="Filter by board ID"
  70. ),
  71. state: Optional[str] = Query(
  72. None,
  73. description="Filter by build state (PENDING, RUNNING, SUCCESS, "
  74. "FAILURE, CANCELLED)"
  75. ),
  76. limit: int = Query(
  77. 20, ge=1, le=100, description="Maximum number of builds to return"
  78. ),
  79. offset: int = Query(
  80. 0, ge=0, description="Number of builds to skip"
  81. ),
  82. service: BuildsService = Depends(get_builds_service)
  83. ):
  84. """
  85. Get list of builds with optional filters.
  86. Args:
  87. vehicle_id: Filter builds by vehicle
  88. board_id: Filter builds by board
  89. state: Filter builds by current state
  90. limit: Maximum number of results
  91. offset: Number of results to skip (for pagination)
  92. Returns:
  93. List of builds matching the filters
  94. """
  95. return service.list_builds(
  96. vehicle_id=vehicle_id,
  97. board_id=board_id,
  98. state=state,
  99. limit=limit,
  100. offset=offset
  101. )
  102. @router.get(
  103. "/{build_id}",
  104. response_model=BuildOut,
  105. responses={
  106. 404: {"description": "Build not found"}
  107. }
  108. )
  109. async def get_build(
  110. build_id: str = Path(..., description="Unique build identifier"),
  111. service: BuildsService = Depends(get_builds_service)
  112. ):
  113. """
  114. Get details of a specific build.
  115. Args:
  116. build_id: The unique build identifier
  117. Returns:
  118. Complete build details including progress and status
  119. Raises:
  120. 404: Build not found
  121. """
  122. build = service.get_build(build_id)
  123. if not build:
  124. raise HTTPException(
  125. status_code=404,
  126. detail=f"Build with id '{build_id}' not found"
  127. )
  128. return build
  129. @router.get(
  130. "/{build_id}/logs",
  131. responses={
  132. 404: {"description": "Build not found or logs not available yet"}
  133. }
  134. )
  135. async def get_build_logs(
  136. build_id: str = Path(..., description="Unique build identifier"),
  137. tail: Optional[int] = Query(
  138. None, ge=1, description="Return only the last N lines"
  139. ),
  140. service: BuildsService = Depends(get_builds_service)
  141. ):
  142. """
  143. Get build logs for a specific build.
  144. Args:
  145. build_id: The unique build identifier
  146. tail: Optional number of last lines to return
  147. Returns:
  148. Build logs as text
  149. Raises:
  150. 404: Build not found
  151. 404: Logs not available yet
  152. """
  153. logs = service.get_build_logs(build_id, tail)
  154. if logs is None:
  155. raise HTTPException(
  156. status_code=404,
  157. detail=f"Logs not available for build '{build_id}'"
  158. )
  159. return PlainTextResponse(content=logs)
  160. @router.get(
  161. "/{build_id}/artifact",
  162. responses={
  163. 404: {
  164. "description": (
  165. "Build not found or artifact not available "
  166. )
  167. }
  168. }
  169. )
  170. async def download_artifact(
  171. build_id: str = Path(..., description="Unique build identifier"),
  172. service: BuildsService = Depends(get_builds_service)
  173. ):
  174. """
  175. Download the build artifact (firmware binary).
  176. Args:
  177. build_id: The unique build identifier
  178. Returns:
  179. Binary file download
  180. Raises:
  181. 404: Build not found
  182. 404: Artifact not available (build not completed)
  183. """
  184. artifact_path = service.get_artifact_path(build_id)
  185. if not artifact_path:
  186. raise HTTPException(
  187. status_code=404,
  188. detail=(
  189. f"Artifact not available for build '{build_id}'. "
  190. "Build may not be completed."
  191. )
  192. )
  193. return FileResponse(
  194. path=artifact_path,
  195. media_type='application/gzip',
  196. filename=f"{build_id}.tar.gz"
  197. )