slicer.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. """Pydantic schemas for slice requests."""
  2. from typing import Literal
  3. from pydantic import BaseModel, Field, model_validator
  4. class PresetRef(BaseModel):
  5. """A source-aware reference to a printer / process / filament preset.
  6. The SliceModal pulls dropdown options from three tiers (cloud / local /
  7. standard). At submit time the client sends one of these per slot so the
  8. backend knows where to fetch the preset content from at slice time.
  9. """
  10. source: Literal["cloud", "local", "standard"]
  11. id: str = Field(..., description=("Cloud setting_id, local DB row id (stringified), or standard preset name."))
  12. class SliceRequest(BaseModel):
  13. """Body for `POST /library/files/{file_id}/slice`.
  14. Two preset shapes are accepted per slot for backwards-compatibility:
  15. - **Legacy** — bare integer ``*_preset_id`` fields point into the
  16. ``local_presets`` table. Existing clients (and stale browser tabs after
  17. a Bambuddy upgrade) keep working unchanged.
  18. - **Source-aware** — ``*_preset`` carries an explicit
  19. ``{source, id}``. Required for cloud / standard tiers; also accepted
  20. (and equivalent) for local presets when the client is on the new modal.
  21. Exactly one of each pair must be set; the validator normalises legacy
  22. integer ids into a ``PresetRef(source='local', id=str(id))`` so the
  23. downstream resolver only deals with one shape.
  24. """
  25. # Legacy fields — kept optional so older clients continue to work.
  26. printer_preset_id: int | None = Field(
  27. default=None,
  28. description="DEPRECATED: prefer printer_preset. LocalPreset id with preset_type='printer'.",
  29. )
  30. process_preset_id: int | None = Field(
  31. default=None,
  32. description="DEPRECATED: prefer process_preset. LocalPreset id with preset_type='process'.",
  33. )
  34. filament_preset_id: int | None = Field(
  35. default=None,
  36. description="DEPRECATED: prefer filament_preset. LocalPreset id with preset_type='filament'.",
  37. )
  38. # Source-aware fields — set by the new SliceModal.
  39. printer_preset: PresetRef | None = None
  40. process_preset: PresetRef | None = None
  41. filament_preset: PresetRef | None = None
  42. plate: int | None = Field(
  43. default=None,
  44. ge=1,
  45. description="Plate number to slice (1-indexed). Defaults to plate 1 on the sidecar.",
  46. )
  47. export_3mf: bool = Field(
  48. default=False,
  49. description="If true, request a 3MF response with embedded G-code instead of raw G-code.",
  50. )
  51. @model_validator(mode="after")
  52. def normalise_preset_refs(self) -> "SliceRequest":
  53. """Each slot must end up with a `PresetRef` set. Legacy integer ids
  54. become `(source='local', id=str(int))` so the route handler only
  55. deals with the canonical shape."""
  56. for slot, ref_attr, legacy_attr in (
  57. ("printer", "printer_preset", "printer_preset_id"),
  58. ("process", "process_preset", "process_preset_id"),
  59. ("filament", "filament_preset", "filament_preset_id"),
  60. ):
  61. ref = getattr(self, ref_attr)
  62. legacy_id = getattr(self, legacy_attr)
  63. if ref is None and legacy_id is None:
  64. raise ValueError(
  65. f"{slot} preset is required: provide '{ref_attr}' (preferred) or legacy '{legacy_attr}'"
  66. )
  67. if ref is None:
  68. setattr(self, ref_attr, PresetRef(source="local", id=str(legacy_id)))
  69. return self
  70. class SliceResponse(BaseModel):
  71. """Response from `POST /library/files/{file_id}/slice`. The result lands
  72. in the user's library as a new ``LibraryFile`` (in the same folder as
  73. the source)."""
  74. library_file_id: int
  75. name: str
  76. print_time_seconds: int
  77. filament_used_g: float
  78. filament_used_mm: float
  79. used_embedded_settings: bool = False
  80. class SliceArchiveResponse(BaseModel):
  81. """Response from `POST /archives/{archive_id}/slice`. The result lands
  82. in the user's archives as a new ``PrintArchive`` row, inheriting
  83. printer / project metadata from the source archive."""
  84. archive_id: int
  85. name: str
  86. print_time_seconds: int
  87. filament_used_g: float
  88. filament_used_mm: float
  89. used_embedded_settings: bool = False