mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
Adds GET /slicer-plugin/info and POST /slicer-plugin/uploads, which take a sliced file plus a JSON manifest in one multipart request: what the user asked for (store, or queue a plate x copies on a printer, optionally held for a manual start) and where the file came from (slicer and plugin versions, presets, a model fingerprint kept for later matching). The manifest does not carry the slice's numbers; plates, time, grams and sliced_for_model are read from the file by the library's own parser. The work is delegated rather than re-implemented: the body of POST /library/files becomes a shared ingest_library_upload, and queueing calls POST /queue/ itself, so every validation, compatibility, filament and budget check applies unchanged. Sends are idempotent on a plugin-chosen upload_id, claimed through a unique constraint, released when the attempt fails, taken over when abandoned, and replayed only to the caller who first used it; a retried queue send is otherwise a second print. A missing plate and a key that may not queue are refused before anything is stored. A refusal from the queue keeps the file, reports queue_error and rolls back what that route wrote first, since with billing on it creates the batch before asking for a cost centre. cost_center_id is accepted for the same reason. New table slicer_plugin_uploads, created on startup.
96 lines
3.4 KiB
Python
96 lines
3.4 KiB
Python
"""Schemas for the slicer plugin's upload API."""
|
|
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, Field, model_validator
|
|
|
|
|
|
class SlicerPluginPresets(BaseModel):
|
|
"""The preset names the slicer had selected when it sliced."""
|
|
|
|
printer: str | None = Field(default=None, max_length=255)
|
|
process: str | None = Field(default=None, max_length=255)
|
|
filaments: list[str] = Field(default_factory=list, max_length=64)
|
|
|
|
|
|
class SlicerPluginSource(BaseModel):
|
|
"""Where the file came from -- what the file itself cannot tell us."""
|
|
|
|
slicer: str | None = Field(default=None, max_length=50)
|
|
slicer_version: str | None = Field(default=None, max_length=50)
|
|
plugin_version: str | None = Field(default=None, max_length=50)
|
|
project_name: str | None = Field(default=None, max_length=255)
|
|
presets: SlicerPluginPresets | None = None
|
|
# A hash of the model's geometry, so a later slice of the same model can
|
|
# find this one however its file is named.
|
|
model_fingerprint: str | None = Field(default=None, max_length=128, pattern=r"^[A-Za-z0-9:_-]+$")
|
|
|
|
|
|
class SlicerPluginIntent(BaseModel):
|
|
"""What the user asked for when they sent the file."""
|
|
|
|
action: Literal["library", "queue"] = "library"
|
|
folder_id: int | None = None
|
|
# Queue only. 1-based, as in the 3MF (Metadata/plate_<n>.gcode).
|
|
plate_id: int | None = Field(default=None, ge=1)
|
|
copies: int = Field(default=1, ge=1, le=100)
|
|
printer_id: int | None = None
|
|
target_model: str | None = Field(default=None, max_length=50)
|
|
manual_start: bool = False
|
|
# Required by POST /queue/ when billing is enabled; ignored otherwise.
|
|
cost_center_id: int | None = None
|
|
|
|
@model_validator(mode="after")
|
|
def _one_destination(self) -> "SlicerPluginIntent":
|
|
if self.printer_id is not None and self.target_model:
|
|
raise ValueError("Cannot specify both printer_id and target_model")
|
|
return self
|
|
|
|
|
|
class SlicerPluginManifest(BaseModel):
|
|
"""The JSON part sent alongside the file.
|
|
|
|
Unknown keys are ignored so an older Bambuddy accepts a newer plugin's
|
|
additions; a change it must understand bumps ``schema_version`` instead.
|
|
"""
|
|
|
|
schema_version: int
|
|
# Chosen by the plugin, one per send; a retry reuses it.
|
|
upload_id: str = Field(min_length=8, max_length=64, pattern=r"^[A-Za-z0-9-]+$")
|
|
source: SlicerPluginSource = Field(default_factory=SlicerPluginSource)
|
|
intent: SlicerPluginIntent = Field(default_factory=SlicerPluginIntent)
|
|
|
|
|
|
class SlicerPluginLibraryFile(BaseModel):
|
|
id: int
|
|
filename: str
|
|
file_type: str
|
|
file_size: int
|
|
thumbnail_path: str | None
|
|
duplicate_of: int | None = None # an active library file with the same bytes
|
|
|
|
|
|
class SlicerPluginQueueResult(BaseModel):
|
|
queue_item_ids: list[int]
|
|
batch_id: int | None = None # set when copies > 1
|
|
|
|
|
|
class SlicerPluginUploadResponse(BaseModel):
|
|
upload_id: str
|
|
library_file: SlicerPluginLibraryFile
|
|
# With action "queue": what was queued, or why nothing was. The file is in
|
|
# the library either way, so the user can still queue it from there.
|
|
queue: SlicerPluginQueueResult | None = None
|
|
queue_error: str | None = None
|
|
warnings: list[str] = []
|
|
# True when this is the stored answer to an earlier request with the same
|
|
# upload_id, not a new upload.
|
|
replayed: bool = False
|
|
|
|
|
|
class SlicerPluginInfoResponse(BaseModel):
|
|
bambuddy_version: str
|
|
manifest_versions: list[int]
|
|
can_upload: bool
|
|
can_queue: bool
|