slicer_pipeline.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. """Pydantic schemas for the Slicer Pipeline API (#1425, PR A).
  2. A pipeline bundles printer / process / filament(s) / bed-type picks under a
  3. reusable name. PR A surfaces only the bundle; target_kind / target_printer_id /
  4. target_model_class / fanout_strategy are persisted but the API treats them as
  5. opaque defaults — they come alive in PR B (single-target dispatch) and PR C
  6. (multi-copy + class targeting + fanout).
  7. """
  8. from datetime import datetime
  9. from typing import Literal
  10. from pydantic import BaseModel, Field, field_validator
  11. from backend.app.schemas.not_null import IgnoresNullForRequiredColumns
  12. from backend.app.schemas.slicer import PresetRef, canonical_bed_type, checked_bed_type
  13. class SlicerPipelineBase(BaseModel):
  14. """Fields editable on create + update."""
  15. name: str = Field(..., min_length=1, max_length=200)
  16. description: str | None = Field(default=None, max_length=1000)
  17. printer_preset: PresetRef
  18. process_preset: PresetRef
  19. filament_presets: list[PresetRef] = Field(
  20. ...,
  21. min_length=1,
  22. description="One PresetRef per AMS slot. Order matches the source plate's filament-slot order.",
  23. )
  24. bed_type: str | None = Field(default=None, max_length=64)
  25. @field_validator("bed_type")
  26. @classmethod
  27. def slicer_bed_type(cls, value: str | None) -> str | None:
  28. # A row saved with an old name reads back as the slicer's name (#3359).
  29. return canonical_bed_type(value)
  30. class SlicerPipelineCreate(SlicerPipelineBase):
  31. """Payload for POST /slicer-pipelines."""
  32. @field_validator("bed_type")
  33. @classmethod
  34. def slicer_bed_type(cls, value: str | None) -> str | None:
  35. return checked_bed_type(value)
  36. class SlicerPipelineUpdate(IgnoresNullForRequiredColumns):
  37. """Payload for PUT /slicer-pipelines/{id}. All fields optional; only those
  38. present are written. Preset and filament list are replaced wholesale when
  39. set (we don't support partial filament-slot edits)."""
  40. __orm_table__ = "slicer_pipelines"
  41. name: str | None = Field(default=None, min_length=1, max_length=200)
  42. description: str | None = Field(default=None, max_length=1000)
  43. printer_preset: PresetRef | None = None
  44. process_preset: PresetRef | None = None
  45. filament_presets: list[PresetRef] | None = Field(default=None, min_length=1)
  46. bed_type: str | None = Field(default=None, max_length=64)
  47. @field_validator("bed_type")
  48. @classmethod
  49. def slicer_bed_type(cls, value: str | None) -> str | None:
  50. return checked_bed_type(value)
  51. # PR B target binding. ``target_kind='specific_printer'`` requires
  52. # ``target_printer_id`` to be set OR cleared in the same payload (route
  53. # handler enforces). ``target_kind='printer_class'`` is wired by PR C
  54. # together with ``target_model_class`` (a Bambu model code like 'X1C')
  55. # and the fanout strategy that distributes copies across matching
  56. # printers.
  57. target_kind: Literal["specific_printer", "printer_class"] | None = None
  58. target_printer_id: int | None = None
  59. target_model_class: str | None = Field(default=None, max_length=20)
  60. fanout_strategy: Literal["max_parallel", "fill_one_first", "round_robin"] | None = None
  61. class SlicerPipelineResponse(SlicerPipelineBase):
  62. """A single pipeline as returned by the API."""
  63. id: int
  64. created_by: int | None
  65. created_at: datetime
  66. updated_at: datetime
  67. # Echoed for PR B+ readiness; PR A always returns the persisted defaults.
  68. target_kind: Literal["specific_printer", "printer_class"] = "printer_class"
  69. target_printer_id: int | None = None
  70. target_model_class: str | None = None
  71. fanout_strategy: Literal["max_parallel", "fill_one_first", "round_robin"] = "max_parallel"
  72. model_config = {"from_attributes": True}
  73. class SlicerPipelineListResponse(BaseModel):
  74. """Wraps the list so the response stays additive when run/job counts get
  75. surfaced in PR B+ (e.g. a ``meta`` field for last-run timestamps)."""
  76. pipelines: list[SlicerPipelineResponse] = []