pipeline_run.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. """Pydantic schemas for PipelineRun + eligibility (#1425 PR B + PR C)."""
  2. from datetime import datetime
  3. from typing import Literal
  4. from pydantic import BaseModel, Field, model_validator
  5. class EligibilityIssueResponse(BaseModel):
  6. """Single eligibility issue — see ``services/pipeline_eligibility.py`` for
  7. the full list of ``kind`` values and what each means."""
  8. kind: Literal[
  9. "printer_not_set",
  10. "printer_not_found",
  11. "printer_disabled",
  12. "printer_offline",
  13. "filament_type_mismatch",
  14. "filament_color_mismatch",
  15. "ams_slot_missing",
  16. "filament_unverified",
  17. "no_class_matches", # PR C: target_kind='printer_class' and zero printers in the install match the model
  18. "class_not_set", # PR C: target_kind='printer_class' with no target_model_class
  19. ]
  20. slot_index: int | None = None
  21. expected: str | None = None
  22. actual: str | None = None
  23. class PerPrinterReport(BaseModel):
  24. """One row of class-targeting eligibility — per matching printer.
  25. PR C extends the top-level report with this list so the confirmation modal
  26. can show ``3 of 5 X1Cs eligible`` plus a per-printer breakdown of why each
  27. candidate is or isn't usable.
  28. """
  29. printer_id: int
  30. printer_name: str
  31. ok: bool
  32. issues: list[EligibilityIssueResponse] = []
  33. class EligibilityReportResponse(BaseModel):
  34. """Returned by both ``POST /check-eligibility`` and (on 409) ``POST /run``
  35. so the frontend can render the same modal in either flow.
  36. ``ok`` semantics:
  37. - ``target_kind='specific_printer'``: ``ok`` mirrors that single
  38. printer's eligibility (no blocking issues).
  39. - ``target_kind='printer_class'``: ``ok`` is True iff **at least one**
  40. matching printer passes — the run can dispatch even if some
  41. candidates in the class are offline / filament-mismatched, because
  42. the scheduler will pick any eligible one. The per-printer list lives
  43. on ``printer_reports`` so the operator sees the full picture.
  44. ``issues`` carries class-level issues only (``no_class_matches``,
  45. ``class_not_set``) — per-printer detail moves to ``printer_reports``.
  46. """
  47. ok: bool
  48. target_kind: Literal["specific_printer", "printer_class"] = "specific_printer"
  49. target_printer_id: int | None = None
  50. target_printer_name: str | None = None
  51. target_model_class: str | None = None
  52. issues: list[EligibilityIssueResponse] = []
  53. printer_reports: list[PerPrinterReport] = []
  54. class CheckEligibilityRequest(BaseModel):
  55. """Exactly one of ``source_library_file_id`` / ``source_archive_id`` must
  56. be set."""
  57. source_library_file_id: int | None = None
  58. source_archive_id: int | None = None
  59. force: bool = Field(default=False)
  60. @model_validator(mode="after")
  61. def exactly_one_source(self) -> "CheckEligibilityRequest":
  62. if (self.source_library_file_id is None) == (self.source_archive_id is None):
  63. raise ValueError("exactly one of source_library_file_id or source_archive_id must be set")
  64. return self
  65. class PipelineRunCreateRequest(BaseModel):
  66. """``copies`` defaults to 1 (PR B parity). The route handler enforces the
  67. ``pipeline_max_copies`` setting on top of the schema's lower bound."""
  68. source_library_file_id: int | None = None
  69. source_archive_id: int | None = None
  70. copies: int = Field(default=1, ge=1, le=1000)
  71. force: bool = Field(
  72. default=False,
  73. description=(
  74. "When False (default), the route returns 409 with the eligibility "
  75. "report if any blocking issue exists. When True, the run starts "
  76. "even when issues exist — recorded on PipelineRun.eligibility_overridden."
  77. ),
  78. )
  79. @model_validator(mode="after")
  80. def exactly_one_source(self) -> "PipelineRunCreateRequest":
  81. if (self.source_library_file_id is None) == (self.source_archive_id is None):
  82. raise ValueError("exactly one of source_library_file_id or source_archive_id must be set")
  83. return self
  84. class PipelineJobResponse(BaseModel):
  85. id: int
  86. pipeline_run_id: int
  87. copy_index: int
  88. assigned_printer_id: int | None
  89. assigned_printer_name: str | None = None
  90. queue_entry_id: int | None
  91. status: Literal[
  92. "pending",
  93. "awaiting_printer",
  94. "queued",
  95. "printing",
  96. "completed",
  97. "failed",
  98. "cancelled",
  99. ]
  100. error_message: str | None = None
  101. dispatched_at: datetime | None = None
  102. completed_at: datetime | None = None
  103. class PipelineRunResponse(BaseModel):
  104. id: int
  105. pipeline_id: int | None
  106. pipeline_name: str | None = None
  107. source_library_file_id: int | None
  108. source_archive_id: int | None = None
  109. source_filename: str | None = None
  110. parent_run_id: int | None = None
  111. copies: int
  112. # Roll-up counts used by the dashboard's per-row summary. Computed at read
  113. # time from the per-job statuses so they always match the live state.
  114. copies_completed: int = 0
  115. copies_failed: int = 0
  116. copies_cancelled: int = 0
  117. copies_in_progress: int = 0
  118. status: Literal[
  119. "queued",
  120. "slicing",
  121. "dispatching",
  122. "in_progress",
  123. "completed",
  124. "failed",
  125. "partial_failure", # PR C: some copies succeeded, some failed/cancelled
  126. "cancelled",
  127. ]
  128. slice_job_id: int | None
  129. sliced_library_file_id: int | None
  130. eligibility_overridden: bool
  131. error_message: str | None = None
  132. created_by: int | None
  133. created_at: datetime
  134. started_at: datetime | None
  135. completed_at: datetime | None
  136. jobs: list[PipelineJobResponse] = []
  137. # Pipeline target snapshot — copied onto the response so the dashboard
  138. # doesn't need a second query to display "Run on X1C class" per row.
  139. target_kind: Literal["specific_printer", "printer_class"] | None = None
  140. target_printer_id: int | None = None
  141. target_model_class: str | None = None
  142. fanout_strategy: Literal["max_parallel", "fill_one_first", "round_robin"] | None = None
  143. class PipelineRunListResponse(BaseModel):
  144. runs: list[PipelineRunResponse] = []
  145. total: int = 0 # PR C: for the dashboard's paginator