routes.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. from datetime import datetime
  2. from typing import TypeVar
  3. from core_domain import ServiceHealth
  4. from core_shared import try_build_redis_client
  5. from fastapi import APIRouter, Depends, HTTPException, Query
  6. from sqlalchemy import text
  7. from sqlalchemy.orm import Session
  8. from app.application.services import TeamApplicationService, build_team_application_service
  9. from app.bootstrap.settings import TeamServiceSettings
  10. from app.db.session import get_db
  11. from app.domain.repositories import (
  12. TeamDefinitionRepository,
  13. TeamRunRepository,
  14. TeamConfigRepository,
  15. )
  16. from app.schemas.team import (
  17. ApiResponse,
  18. DeleteData,
  19. PageResult,
  20. TeamConfigCreateRequestDto,
  21. TeamConfigDeleteRequestDto,
  22. TeamConfigDetailRequestDto,
  23. TeamConfigDto,
  24. TeamConfigListRequestDto,
  25. TeamConfigUpdateRequestDto,
  26. TeamCreateRequest,
  27. TeamCreateRequestDto,
  28. TeamDeleteRequestDto,
  29. TeamDetailRequestDto,
  30. TeamDto,
  31. TeamListRequestDto,
  32. TeamResponse,
  33. TeamRunCreateRequest,
  34. TeamRunCreateRequestDto,
  35. TeamRunDeleteRequestDto,
  36. TeamRunDetailRequestDto,
  37. TeamRunDto,
  38. TeamRunExecuteRequest,
  39. TeamRunExecuteData,
  40. TeamRunExecuteRequestDto,
  41. TeamRunExecuteResponse,
  42. TeamRunListRequestDto,
  43. TeamRunResponse,
  44. TeamRunStatusUpdateRequest,
  45. TeamRunStatusUpdateRequestDto,
  46. TeamStatusUpdateRequest,
  47. TeamUpdateRequestDto,
  48. TeamWorkerExecuteNextRequest,
  49. TeamWorkerExecuteNextResponse,
  50. )
  51. router = APIRouter()
  52. T = TypeVar("T")
  53. def ok(data: T) -> ApiResponse[T]:
  54. return ApiResponse[T](
  55. data=data,
  56. requestId="",
  57. serverTime=datetime.utcnow())
  58. def get_team_settings() -> TeamServiceSettings:
  59. return TeamServiceSettings()
  60. def get_team_application_service(
  61. db: Session = Depends(get_db),
  62. settings: TeamServiceSettings = Depends(get_team_settings)) -> TeamApplicationService:
  63. return build_team_application_service(
  64. team_repository=TeamDefinitionRepository(db),
  65. team_config_repository=TeamConfigRepository(db),
  66. team_run_repository=TeamRunRepository(db),
  67. settings=settings)
  68. @router.get("/health", response_model=ServiceHealth)
  69. def health_check(db: Session = Depends(get_db)) -> ServiceHealth:
  70. db.execute(text("SELECT 1"))
  71. return ServiceHealth(service="team-service", status="ok", database="ok")
  72. @router.post("", response_model=TeamResponse)
  73. def create_team(
  74. payload: TeamCreateRequest,
  75. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamResponse:
  76. entity = service.create_team(payload)
  77. return TeamResponse.from_entity(entity)
  78. @router.get("", response_model=list[TeamResponse])
  79. def list_teams(
  80. service: TeamApplicationService = Depends(get_team_application_service)) -> list[TeamResponse]:
  81. return [TeamResponse.from_entity(item) for item in service.list_teams()]
  82. @router.post("/list", response_model=ApiResponse[PageResult[TeamDto]])
  83. def list_teams_contract(
  84. payload: TeamListRequestDto,
  85. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[PageResult[TeamDto]]:
  86. keyword = (payload.keyword or "").lower().strip()
  87. items = [
  88. item
  89. for item in service.list_teams()
  90. if (payload.status is None or item.status == payload.status)
  91. and (
  92. not keyword
  93. or keyword in item.name.lower()
  94. or keyword in item.team_type.lower()
  95. or keyword in (item.description or "").lower()
  96. )
  97. ]
  98. page_items = items[payload.offset:payload.offset + payload.pageSize]
  99. return ok(PageResult[TeamDto].from_items(
  100. items=[TeamDto.from_entity(item) for item in page_items],
  101. total=len(items),
  102. page=payload.page,
  103. page_size=payload.pageSize))
  104. @router.post("/create", response_model=ApiResponse[TeamDto])
  105. def create_team_contract(
  106. payload: TeamCreateRequestDto,
  107. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamDto]:
  108. return ok(TeamDto.from_entity(service.create_team_from_contract(payload)))
  109. @router.post("/detail", response_model=ApiResponse[TeamDto])
  110. def get_team_contract(
  111. payload: TeamDetailRequestDto,
  112. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamDto]:
  113. entity = service.get_team(team_id=payload.teamId)
  114. if entity is None:
  115. raise HTTPException(status_code=404, detail=f"team not found: {payload.teamId}")
  116. return ok(TeamDto.from_entity(entity))
  117. @router.post("/update", response_model=ApiResponse[TeamDto])
  118. def update_team_contract(
  119. payload: TeamUpdateRequestDto,
  120. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamDto]:
  121. entity = service.update_team_from_contract(payload)
  122. if entity is None:
  123. raise HTTPException(status_code=404, detail=f"team not found: {payload.teamId}")
  124. return ok(TeamDto.from_entity(entity))
  125. @router.post("/delete", response_model=ApiResponse[DeleteData])
  126. def delete_team_contract(
  127. payload: TeamDeleteRequestDto,
  128. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[DeleteData]:
  129. deleted = service.delete_team_from_contract(payload)
  130. return ok(DeleteData(deleted=deleted, teamId=payload.teamId))
  131. @router.patch("/{team_id}/status", response_model=TeamResponse)
  132. def update_team_status(
  133. team_id: str,
  134. payload: TeamStatusUpdateRequest,
  135. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamResponse:
  136. entity = service.update_team_status(team_id=team_id, payload=payload)
  137. if entity is None:
  138. raise HTTPException(status_code=404, detail=f"team not found: {team_id}")
  139. return TeamResponse.from_entity(entity)
  140. @router.post("/configs/list", response_model=ApiResponse[PageResult[TeamConfigDto]])
  141. def list_team_configs_contract(
  142. payload: TeamConfigListRequestDto,
  143. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[PageResult[TeamConfigDto]]:
  144. items = service.list_team_configs(team_id=payload.teamId)
  145. page_items = items[payload.offset:payload.offset + payload.pageSize]
  146. return ok(PageResult[TeamConfigDto].from_items(
  147. items=[TeamConfigDto.from_entity(item) for item in page_items],
  148. total=len(items),
  149. page=payload.page,
  150. page_size=payload.pageSize))
  151. @router.post("/configs/create", response_model=ApiResponse[TeamConfigDto])
  152. def create_team_config_contract(
  153. payload: TeamConfigCreateRequestDto,
  154. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamConfigDto]:
  155. try:
  156. entity = service.create_team_config_from_contract(payload)
  157. except ValueError as exc:
  158. raise HTTPException(status_code=422, detail=str(exc)) from exc
  159. return ok(TeamConfigDto.from_entity(entity))
  160. @router.post("/configs/detail", response_model=ApiResponse[TeamConfigDto])
  161. def get_team_config_contract(
  162. payload: TeamConfigDetailRequestDto,
  163. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamConfigDto]:
  164. entity = service.get_team_config(config_id=payload.configId)
  165. if entity is None:
  166. raise HTTPException(status_code=404, detail=f"team config not found: {payload.configId}")
  167. return ok(TeamConfigDto.from_entity(entity))
  168. @router.post("/configs/update", response_model=ApiResponse[TeamConfigDto])
  169. def update_team_config_contract(
  170. payload: TeamConfigUpdateRequestDto,
  171. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamConfigDto]:
  172. try:
  173. entity = service.update_team_config_from_contract(payload)
  174. except ValueError as exc:
  175. raise HTTPException(status_code=422, detail=str(exc)) from exc
  176. if entity is None:
  177. raise HTTPException(status_code=404, detail=f"team config not found: {payload.configId}")
  178. return ok(TeamConfigDto.from_entity(entity))
  179. @router.post("/configs/delete", response_model=ApiResponse[DeleteData])
  180. def delete_team_config_contract(
  181. payload: TeamConfigDeleteRequestDto,
  182. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[DeleteData]:
  183. deleted = service.delete_team_config(config_id=payload.configId)
  184. return ok(DeleteData(deleted=deleted, configId=payload.configId))
  185. @router.post("/runs", response_model=TeamRunResponse)
  186. def create_team_run(
  187. payload: TeamRunCreateRequest,
  188. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamRunResponse:
  189. try:
  190. entity = service.create_team_run(payload)
  191. except ValueError as exc:
  192. raise HTTPException(status_code=422, detail=str(exc)) from exc
  193. return TeamRunResponse.from_entity(entity)
  194. @router.get("/runs", response_model=list[TeamRunResponse])
  195. def list_team_runs(
  196. team_id: str | None = Query(default=None),
  197. session_id: str | None = Query(default=None),
  198. service: TeamApplicationService = Depends(get_team_application_service)) -> list[TeamRunResponse]:
  199. return [
  200. TeamRunResponse.from_entity(item)
  201. for item in service.list_team_runs(
  202. team_id=team_id,
  203. session_id=session_id)
  204. ]
  205. @router.post("/runs/list", response_model=ApiResponse[PageResult[TeamRunDto]])
  206. def list_team_runs_contract(
  207. payload: TeamRunListRequestDto,
  208. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[PageResult[TeamRunDto]]:
  209. items = [
  210. item
  211. for item in service.list_team_runs(team_id=payload.teamId, session_id=payload.sessionId)
  212. if payload.status is None or item.status == payload.status
  213. ]
  214. page_items = items[payload.offset:payload.offset + payload.pageSize]
  215. return ok(PageResult[TeamRunDto].from_items(
  216. items=[TeamRunDto.from_entity(item) for item in page_items],
  217. total=len(items),
  218. page=payload.page,
  219. page_size=payload.pageSize))
  220. @router.post("/runs/create", response_model=ApiResponse[TeamRunDto])
  221. def create_team_run_contract(
  222. payload: TeamRunCreateRequestDto,
  223. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamRunDto]:
  224. try:
  225. entity = service.create_team_run_from_contract(payload)
  226. except ValueError as exc:
  227. raise HTTPException(status_code=422, detail=str(exc)) from exc
  228. return ok(TeamRunDto.from_entity(entity))
  229. @router.post("/runs/detail", response_model=ApiResponse[TeamRunDto])
  230. def get_team_run_contract(
  231. payload: TeamRunDetailRequestDto,
  232. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamRunDto]:
  233. entity = service.get_team_run(team_run_id=payload.teamRunId)
  234. if entity is None:
  235. raise HTTPException(status_code=404, detail=f"team_run not found: {payload.teamRunId}")
  236. return ok(TeamRunDto.from_entity(entity))
  237. @router.post("/runs/status", response_model=ApiResponse[TeamRunDto])
  238. def update_team_run_status_contract(
  239. payload: TeamRunStatusUpdateRequestDto,
  240. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamRunDto]:
  241. entity = service.update_team_run_status_from_contract(payload)
  242. if entity is None:
  243. raise HTTPException(status_code=404, detail=f"team_run not found: {payload.teamRunId}")
  244. return ok(TeamRunDto.from_entity(entity))
  245. @router.post("/runs/execute", response_model=ApiResponse[TeamRunExecuteData])
  246. def execute_team_run_contract(
  247. payload: TeamRunExecuteRequestDto,
  248. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[TeamRunExecuteData]:
  249. entity = service.execute_team_run(
  250. team_run_id=payload.teamRunId,
  251. payload=TeamRunExecuteRequest(
  252. worker_key=payload.workerKey,
  253. dry_run=payload.dryRun))
  254. if entity is None:
  255. raise HTTPException(status_code=404, detail=f"team_run not found: {payload.teamRunId}")
  256. output_json = entity.output_json or {}
  257. member_run_count = output_json.get("member_run_count")
  258. dry_run = output_json.get("dry_run")
  259. return ok(TeamRunExecuteData(
  260. run=TeamRunDto.from_entity(entity),
  261. memberRunCount=member_run_count if isinstance(member_run_count, int) else 0,
  262. dryRun=dry_run if isinstance(dry_run, bool) else payload.dryRun))
  263. @router.post("/runs/delete", response_model=ApiResponse[DeleteData])
  264. def delete_team_run_contract(
  265. payload: TeamRunDeleteRequestDto,
  266. service: TeamApplicationService = Depends(get_team_application_service)) -> ApiResponse[DeleteData]:
  267. deleted = service.delete_team_run(team_run_id=payload.teamRunId)
  268. return ok(DeleteData(deleted=deleted, teamRunId=payload.teamRunId))
  269. @router.post("/runs/{team_run_id}/status", response_model=TeamRunResponse)
  270. def update_team_run_status(
  271. team_run_id: str,
  272. payload: TeamRunStatusUpdateRequest,
  273. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamRunResponse:
  274. entity = service.update_team_run_status(team_run_id=team_run_id, payload=payload)
  275. if entity is None:
  276. raise HTTPException(status_code=404, detail=f"team_run not found: {team_run_id}")
  277. return TeamRunResponse.from_entity(entity)
  278. @router.post("/runs/{team_run_id}/execute", response_model=TeamRunExecuteResponse)
  279. def execute_team_run(
  280. team_run_id: str,
  281. payload: TeamRunExecuteRequest,
  282. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamRunExecuteResponse:
  283. entity = service.execute_team_run(team_run_id=team_run_id, payload=payload)
  284. if entity is None:
  285. raise HTTPException(status_code=404, detail=f"team_run not found: {team_run_id}")
  286. output_json = entity.output_json or {}
  287. member_run_count = output_json.get("member_run_count")
  288. dry_run = output_json.get("dry_run")
  289. return TeamRunExecuteResponse(
  290. run=TeamRunResponse.from_entity(entity),
  291. member_run_count=member_run_count if isinstance(member_run_count, int) else 0,
  292. dry_run=dry_run if isinstance(dry_run, bool) else payload.dry_run)
  293. @router.post("/workers/execute-next", response_model=TeamWorkerExecuteNextResponse)
  294. def execute_next_worker_task(
  295. payload: TeamWorkerExecuteNextRequest,
  296. settings: TeamServiceSettings = Depends(get_team_settings),
  297. service: TeamApplicationService = Depends(get_team_application_service)) -> TeamWorkerExecuteNextResponse:
  298. result = service.execute_next_claimed_team_run(
  299. worker_key=payload.worker_key,
  300. lease_seconds=payload.lease_seconds or settings.worker_lease_seconds,
  301. stale_running_seconds=settings.worker_stale_running_seconds,
  302. dry_run=payload.dry_run if payload.dry_run is not None else settings.worker_dry_run,
  303. redis_client=try_build_redis_client(settings.redis_url))
  304. if result is None:
  305. raise HTTPException(status_code=404, detail="queued team_run not found")
  306. entity, released_lease_count = result
  307. output_json = entity.output_json or {}
  308. member_run_count = output_json.get("member_run_count")
  309. dry_run = output_json.get("dry_run")
  310. return TeamWorkerExecuteNextResponse(
  311. run=TeamRunResponse.from_entity(entity),
  312. member_run_count=member_run_count if isinstance(member_run_count, int) else 0,
  313. dry_run=dry_run if isinstance(dry_run, bool) else settings.worker_dry_run,
  314. released_lease_count=released_lease_count)