| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- """Pydantic schemas for slice requests."""
- from typing import Literal
- from pydantic import BaseModel, Field, model_validator
- class PresetRef(BaseModel):
- """A source-aware reference to a printer / process / filament preset.
- The SliceModal pulls dropdown options from three tiers (cloud / local /
- standard). At submit time the client sends one of these per slot so the
- backend knows where to fetch the preset content from at slice time.
- """
- source: Literal["cloud", "local", "standard"]
- id: str = Field(..., description=("Cloud setting_id, local DB row id (stringified), or standard preset name."))
- class SliceBundleSpec(BaseModel):
- """Per-request reference to a Printer Preset Bundle stored on the slicer
- sidecar. When SliceRequest.bundle is set, the dispatch skips PresetRef
- resolution entirely and asks the sidecar to pick its inner JSON triplet
- by name from the bundle's extracted directory — much faster than
- re-uploading three profile JSONs every slice and matches the preset
- triplet the user actually slices with in BambuStudio.
- """
- bundle_id: str = Field(
- ...,
- min_length=1,
- description="Sidecar-side bundle id from POST /api/v1/slicer/bundles.",
- )
- printer_name: str = Field(
- ...,
- min_length=1,
- description="Preset name within the bundle's printer/ directory (with or without the BambuStudio '# ' prefix).",
- )
- process_name: str = Field(
- ...,
- min_length=1,
- description="Preset name within the bundle's process/ directory.",
- )
- filament_names: list[str] = Field(
- ...,
- min_length=1,
- description="Per-slot filament preset names within the bundle's filament/ directory. Index 0 = slot 1.",
- )
- class SliceRequest(BaseModel):
- """Body for `POST /library/files/{file_id}/slice`.
- Two preset shapes are accepted per slot for backwards-compatibility:
- - **Legacy** — bare integer ``*_preset_id`` fields point into the
- ``local_presets`` table. Existing clients (and stale browser tabs after
- a Bambuddy upgrade) keep working unchanged.
- - **Source-aware** — ``*_preset`` carries an explicit
- ``{source, id}``. Required for cloud / standard tiers; also accepted
- (and equivalent) for local presets when the client is on the new modal.
- Exactly one of each pair must be set; the validator normalises legacy
- integer ids into a ``PresetRef(source='local', id=str(id))`` so the
- downstream resolver only deals with one shape.
- """
- # Legacy fields — kept optional so older clients continue to work.
- printer_preset_id: int | None = Field(
- default=None,
- description="DEPRECATED: prefer printer_preset. LocalPreset id with preset_type='printer'.",
- )
- process_preset_id: int | None = Field(
- default=None,
- description="DEPRECATED: prefer process_preset. LocalPreset id with preset_type='process'.",
- )
- filament_preset_id: int | None = Field(
- default=None,
- description="DEPRECATED: prefer filament_preset. LocalPreset id with preset_type='filament'.",
- )
- # Source-aware fields — set by the new SliceModal.
- printer_preset: PresetRef | None = None
- process_preset: PresetRef | None = None
- filament_preset: PresetRef | None = None
- # Multi-color: one PresetRef per AMS slot the source plate uses. Order is
- # significant — the slicer matches index-by-index against the plate's
- # filament slots. Always preferred over the legacy singular field; the
- # validator promotes a singular field into ``[singular]`` when the list
- # is empty so older clients keep working.
- filament_presets: list[PresetRef] = Field(default_factory=list)
- # Bundle dispatch alternative — when set, presets above are ignored and
- # the slicer dispatch picks per-category JSONs from a previously-imported
- # .bbscfg on the sidecar. Validator below short-circuits the
- # presets-required check when this is non-None.
- bundle: SliceBundleSpec | None = Field(
- default=None,
- description="When set, slice via a sidecar-side bundle instead of resolved preset refs.",
- )
- plate: int | None = Field(
- default=None,
- ge=0,
- description=(
- "Plate number to slice. ``None`` defaults to plate 1 on the sidecar "
- "(matches the pre-multi-plate behaviour). ``0`` is the sidecar's "
- "'all plates' sentinel — produces a single multi-plate 3MF whose "
- "``Metadata/plate_N.gcode`` entries cover every plate in the "
- "source. ``>= 1`` slices that one plate."
- ),
- )
- export_3mf: bool = Field(
- default=False,
- description="If true, request a 3MF response with embedded G-code instead of raw G-code.",
- )
- bed_type: str | None = Field(
- default=None,
- max_length=64,
- description=(
- "Override the process preset's curr_bed_type for this slice. Canonical "
- "BambuStudio / OrcaSlicer values: 'Cool Plate', 'Engineering Plate', "
- "'High Temp Plate', 'Textured PEI Plate', 'Smooth PEI Plate', "
- "'Cool Plate (SuperTack)', 'Supertack Plate'. None ⇒ inherit from the "
- "process preset unchanged (#1337)."
- ),
- )
- @model_validator(mode="after")
- def normalise_preset_refs(self) -> "SliceRequest":
- """Each slot must end up with a `PresetRef` set. Legacy integer ids
- become `(source='local', id=str(int))` so the route handler only
- deals with the canonical shape. For filament: a non-empty
- ``filament_presets`` list satisfies the requirement on its own; an
- empty list falls back to the singular fields, which then promote
- into a one-element list.
- When ``bundle`` is set, the dispatch picks the JSON triplet from
- the sidecar bundle directly so PresetRef resolution is skipped —
- return early before the presets-required checks below.
- """
- if self.bundle is not None:
- return self
- for slot, ref_attr, legacy_attr in (
- ("printer", "printer_preset", "printer_preset_id"),
- ("process", "process_preset", "process_preset_id"),
- ):
- ref = getattr(self, ref_attr)
- legacy_id = getattr(self, legacy_attr)
- if ref is None and legacy_id is None:
- raise ValueError(
- f"{slot} preset is required: provide '{ref_attr}' (preferred) or legacy '{legacy_attr}'"
- )
- if ref is None:
- setattr(self, ref_attr, PresetRef(source="local", id=str(legacy_id)))
- # Filament accepts THREE shapes, in priority order:
- # 1. filament_presets — multi-color array (new clients)
- # 2. filament_preset — source-aware singular (single-color new clients)
- # 3. filament_preset_id — legacy bare integer (old clients)
- # The first non-empty shape wins; missing all three raises.
- if not self.filament_presets:
- if self.filament_preset is not None:
- self.filament_presets = [self.filament_preset]
- elif self.filament_preset_id is not None:
- fallback = PresetRef(source="local", id=str(self.filament_preset_id))
- self.filament_preset = fallback
- self.filament_presets = [fallback]
- else:
- raise ValueError(
- "filament preset is required: provide 'filament_presets' (preferred), "
- "'filament_preset', or legacy 'filament_preset_id'"
- )
- elif self.filament_preset is None:
- # Multi-color caller: backfill the singular from the first slot
- # so callers that still read the legacy field see a stable value.
- self.filament_preset = self.filament_presets[0]
- return self
- class SliceResponse(BaseModel):
- """Response from `POST /library/files/{file_id}/slice`. The result lands
- in the user's library as a new ``LibraryFile`` (in the same folder as
- the source)."""
- library_file_id: int
- name: str
- print_time_seconds: int
- filament_used_g: float
- filament_used_mm: float
- used_embedded_settings: bool = False
- class SliceArchiveResponse(BaseModel):
- """Response from `POST /archives/{archive_id}/slice`. The result lands
- in the user's archives as a new ``PrintArchive`` row, inheriting
- printer / project metadata from the source archive."""
- archive_id: int
- name: str
- print_time_seconds: int
- filament_used_g: float
- filament_used_mm: float
- used_embedded_settings: bool = False
|