"""New spools in Spoolman from the bridge UI: an existing filament, or a new one (and its vendor).""" from __future__ import annotations import re from .spoolman import Spoolman, make_label MAX_COUNT = 20 class NewSpoolError(ValueError): pass def _num(body: dict, key: str, lo: float, hi: float, required: bool = True, what: str = "") -> float | None: raw = body.get(key) if raw in (None, ""): if required: raise NewSpoolError(f"{what or key} fehlt") return None try: v = float(raw) except (TypeError, ValueError): raise NewSpoolError(f"{what or key}: keine Zahl") from None if not lo <= v <= hi: raise NewSpoolError(f"{what or key} muss zwischen {lo:g} und {hi:g} liegen") return v def _text(body: dict, key: str, what: str, limit: int = 64) -> str: v = re.sub(r"\s+", " ", str(body.get(key) or "")).strip() if not v: raise NewSpoolError(f"{what} fehlt") if len(v) > limit: raise NewSpoolError(f"{what} ist zu lang (höchstens {limit} Zeichen)") return v def catalog(client: Spoolman) -> dict: """Vendors and filaments for the form, and the id Spoolman will most likely give the next spool.""" vendors = sorted(client.vendors(), key=lambda v: v["name"].lower()) spools = client.spools(archived=True) counts: dict[int, int] = {} for sp in spools: if not sp.get("archived"): fid = (sp.get("filament") or {}).get("id") counts[fid] = counts.get(fid, 0) + 1 filaments = [] for f in client.filaments(): if f.get("archived"): continue filaments.append({ "id": f["id"], "name": f.get("name") or "", "vendor": (f.get("vendor") or {}).get("name") or "", "material": f.get("material") or "", "color": (f.get("color_hex") or "").upper()[:6], "density": f.get("density"), "diameter": f.get("diameter"), "weight": f.get("weight"), "spool_weight": f.get("spool_weight"), "extruder_temp": f.get("settings_extruder_temp"), "bed_temp": f.get("settings_bed_temp"), "price": f.get("price"), "spools": counts.get(f["id"], 0), }) filaments.sort(key=lambda f: (f["vendor"].lower(), f["name"].lower())) next_id = max((int(sp["id"]) for sp in spools), default=0) + 1 return {"vendors": [{"id": v["id"], "name": v["name"], "empty_spool_weight": v.get("empty_spool_weight")} for v in vendors], "filaments": filaments, "next_id": next_id} def plan(body: dict) -> dict: """Checks the form; returns what to create.""" count = int(_num(body, "count", 1, MAX_COUNT, what="Anzahl")) out: dict = {"count": count, "initial_weight": _num(body, "weight", 1, 20000, required=False, what="Nettogewicht"), "spool_weight": _num(body, "spool_weight", 0, 5000, required=False, what="Leergewicht"), "price": _num(body, "price", 0, 1e6, required=False, what="Preis")} if body.get("filament_id") not in (None, ""): out["filament_id"] = int(body["filament_id"]) return out color = str(body.get("color") or "").strip().lstrip("#").upper() if not re.fullmatch(r"[0-9A-F]{6}", color): raise NewSpoolError("Farbe als RRGGBB angeben") out["vendor"] = _text(body, "vendor", "Hersteller") out["filament"] = { "name": _text(body, "name", "Name", 96), "material": _text(body, "material", "Material", 32), "color_hex": color, "density": _num(body, "density", 0.5, 3, what="Dichte"), "diameter": _num(body, "diameter", 1, 3.5, what="Durchmesser"), "weight": out["initial_weight"], "spool_weight": out["spool_weight"], "settings_extruder_temp": _int(_num(body, "extruder_temp", 150, 450, required=False, what="Düsentemperatur")), "settings_bed_temp": _int(_num(body, "bed_temp", 0, 150, required=False, what="Betttemperatur")), "price": out["price"], } return out def _int(v: float | None) -> int | None: return None if v is None else int(round(v)) def describe(p: dict, filament_name: str, vendor: str) -> str: n = p["count"] weight = f", {p['initial_weight']:g} g" if p.get("initial_weight") else "" return f"{n} Spule{'n' if n > 1 else ''} {vendor} {filament_name}{weight}" def create(client: Spoolman, p: dict, location: str) -> tuple[list[dict], str]: """Creates what `plan` returned. Returns the new spools (id, label) and what was created, for the log.""" created_parts = [] if "filament_id" in p: fil = next((f for f in client.filaments() if f["id"] == p["filament_id"]), None) if fil is None: raise NewSpoolError("Filament gibt es in Spoolman nicht mehr") vendor = (fil.get("vendor") or {}).get("name") or "" else: vendor = p["vendor"] v = next((v for v in client.vendors() if v["name"].strip().lower() == vendor.lower()), None) if v is None: body = {"name": vendor} if p["filament"].get("spool_weight") is not None: body["empty_spool_weight"] = p["filament"]["spool_weight"] v = client.create_vendor(body) created_parts.append(f"Hersteller {vendor}") vendor = v["name"] fbody = {k: val for k, val in p["filament"].items() if val is not None} fbody["vendor_id"] = v["id"] fil = client.create_filament(fbody) created_parts.append(f"Filament {fil.get('name')}") spools = [] for _ in range(p["count"]): body: dict = {"filament_id": fil["id"], "location": location} for key in ("initial_weight", "spool_weight", "price"): if p.get(key) is not None: body[key] = p[key] sp = client.create_spool(body) lot = make_label(vendor, int(sp["id"])) client.update_spool(int(sp["id"]), {"lot_nr": lot}) spools.append({"id": int(sp["id"]), "label": lot}) created_parts.insert(0, describe(p, fil.get("name") or "", vendor) + " (" + ", ".join(s["label"] for s in spools) + ")") return spools, "; ".join(created_parts)