|
@@ -0,0 +1,166 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+"""Import spools from Spoolman into Bambuddy inventory.
|
|
|
|
|
+
|
|
|
|
|
+Usage:
|
|
|
|
|
+ python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000
|
|
|
|
|
+ python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000 --api-key YOUR_KEY
|
|
|
|
|
+ python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000 --dry-run
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import argparse
|
|
|
|
|
+import sys
|
|
|
|
|
+
|
|
|
|
|
+import requests
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def fetch_spoolman_spools(spoolman_url: str) -> list[dict]:
|
|
|
|
|
+ """Fetch all spools from Spoolman API."""
|
|
|
|
|
+ url = f"{spoolman_url.rstrip('/')}/api/v1/spool"
|
|
|
|
|
+ resp = requests.get(url, timeout=30)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ return resp.json()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def map_spool(sm_spool: dict) -> dict:
|
|
|
|
|
+ """Map a Spoolman spool to a Bambuddy SpoolCreate payload."""
|
|
|
|
|
+ filament = sm_spool.get("filament") or {}
|
|
|
|
|
+ vendor = filament.get("vendor") or {}
|
|
|
|
|
+
|
|
|
|
|
+ material = filament.get("material") or "PLA"
|
|
|
|
|
+ color_hex = filament.get("color_hex") or ""
|
|
|
|
|
+ # Spoolman color_hex is 6-char (#RRGGBB or RRGGBB), Bambuddy rgba is 8-char RRGGBBAA
|
|
|
|
|
+ rgba = None
|
|
|
|
|
+ if color_hex:
|
|
|
|
|
+ color_hex = color_hex.lstrip("#")
|
|
|
|
|
+ if len(color_hex) == 6:
|
|
|
|
|
+ rgba = f"{color_hex}FF"
|
|
|
|
|
+ elif len(color_hex) == 8:
|
|
|
|
|
+ rgba = color_hex
|
|
|
|
|
+
|
|
|
|
|
+ label_weight = int(filament.get("weight") or 1000)
|
|
|
|
|
+ used_weight = float(sm_spool.get("used_weight") or 0)
|
|
|
|
|
+
|
|
|
|
|
+ # Filament name from Spoolman (e.g. "eSun PLA+ Black")
|
|
|
|
|
+ filament_name = filament.get("name") or ""
|
|
|
|
|
+ # Vendor name (e.g. "eSun", "Bambu Lab")
|
|
|
|
|
+ brand = vendor.get("name")
|
|
|
|
|
+
|
|
|
|
|
+ # Color name - prefer filament color name if present
|
|
|
|
|
+ color_name = filament.get("color_hex_name") or None
|
|
|
|
|
+
|
|
|
|
|
+ # Cost: Spoolman stores price per spool, we need cost per kg
|
|
|
|
|
+ cost_per_kg = None
|
|
|
|
|
+ spool_price = sm_spool.get("price") or filament.get("price")
|
|
|
|
|
+ if spool_price and label_weight > 0:
|
|
|
|
|
+ cost_per_kg = round(float(spool_price) / (label_weight / 1000), 2)
|
|
|
|
|
+
|
|
|
|
|
+ # Temperature range from filament settings
|
|
|
|
|
+ nozzle_temp_min = filament.get("settings", {}).get("nozzle_temperature_min") if filament.get("settings") else None
|
|
|
|
|
+ nozzle_temp_max = filament.get("settings", {}).get("nozzle_temperature_max") if filament.get("settings") else None
|
|
|
|
|
+
|
|
|
|
|
+ # Extra fields
|
|
|
|
|
+ extra = sm_spool.get("extra") or {}
|
|
|
|
|
+ tag_uid = extra.get("tag") or None
|
|
|
|
|
+
|
|
|
|
|
+ # Build note with Spoolman reference
|
|
|
|
|
+ note_parts = []
|
|
|
|
|
+ if sm_spool.get("comment"):
|
|
|
|
|
+ note_parts.append(sm_spool["comment"])
|
|
|
|
|
+ if sm_spool.get("lot_nr"):
|
|
|
|
|
+ note_parts.append(f"Lot: {sm_spool['lot_nr']}")
|
|
|
|
|
+ note_parts.append(f"Imported from Spoolman (ID: {sm_spool['id']})")
|
|
|
|
|
+ note = " | ".join(note_parts)
|
|
|
|
|
+
|
|
|
|
|
+ payload = {
|
|
|
|
|
+ "material": material,
|
|
|
|
|
+ "color_name": color_name,
|
|
|
|
|
+ "rgba": rgba.upper() if rgba else None,
|
|
|
|
|
+ "brand": brand,
|
|
|
|
|
+ "label_weight": label_weight,
|
|
|
|
|
+ "weight_used": used_weight,
|
|
|
|
|
+ "note": note,
|
|
|
|
|
+ "cost_per_kg": cost_per_kg,
|
|
|
|
|
+ "tag_uid": tag_uid,
|
|
|
|
|
+ "data_origin": "spoolman",
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if filament_name:
|
|
|
|
|
+ payload["subtype"] = filament_name
|
|
|
|
|
+
|
|
|
|
|
+ if nozzle_temp_min is not None:
|
|
|
|
|
+ payload["nozzle_temp_min"] = int(nozzle_temp_min)
|
|
|
|
|
+ if nozzle_temp_max is not None:
|
|
|
|
|
+ payload["nozzle_temp_max"] = int(nozzle_temp_max)
|
|
|
|
|
+
|
|
|
|
|
+ return payload
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def create_bambuddy_spool(bambuddy_url: str, spool_data: dict, api_key: str | None = None) -> dict:
|
|
|
|
|
+ """Create a spool in Bambuddy inventory."""
|
|
|
|
|
+ url = f"{bambuddy_url.rstrip('/')}/api/v1/inventory/spools"
|
|
|
|
|
+ headers = {}
|
|
|
|
|
+ if api_key:
|
|
|
|
|
+ headers["X-API-Key"] = api_key
|
|
|
|
|
+ resp = requests.post(url, json=spool_data, headers=headers, timeout=30)
|
|
|
|
|
+ resp.raise_for_status()
|
|
|
|
|
+ return resp.json()
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ parser = argparse.ArgumentParser(description="Import spools from Spoolman into Bambuddy inventory")
|
|
|
|
|
+ parser.add_argument("--spoolman-url", required=True, help="Spoolman URL (e.g. http://localhost:7912)")
|
|
|
|
|
+ parser.add_argument("--bambuddy-url", required=True, help="Bambuddy URL (e.g. http://localhost:8000)")
|
|
|
|
|
+ parser.add_argument("--api-key", help="Bambuddy API key (required if auth is enabled)")
|
|
|
|
|
+ parser.add_argument("--dry-run", action="store_true", help="Print mapped spools without importing")
|
|
|
|
|
+ parser.add_argument("--archived", action="store_true", help="Include archived Spoolman spools")
|
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ print(f"Fetching spools from {args.spoolman_url}...")
|
|
|
|
|
+ try:
|
|
|
|
|
+ sm_spools = fetch_spoolman_spools(args.spoolman_url)
|
|
|
|
|
+ except requests.RequestException as e:
|
|
|
|
|
+ print(f"Error fetching from Spoolman: {e}", file=sys.stderr)
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ if not args.archived:
|
|
|
|
|
+ sm_spools = [s for s in sm_spools if not s.get("archived")]
|
|
|
|
|
+
|
|
|
|
|
+ print(f"Found {len(sm_spools)} spools in Spoolman")
|
|
|
|
|
+
|
|
|
|
|
+ if not sm_spools:
|
|
|
|
|
+ print("Nothing to import.")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ created = 0
|
|
|
|
|
+ failed = 0
|
|
|
|
|
+
|
|
|
|
|
+ for sm_spool in sm_spools:
|
|
|
|
|
+ filament = sm_spool.get("filament") or {}
|
|
|
|
|
+ vendor = (filament.get("vendor") or {}).get("name", "?")
|
|
|
|
|
+ name = filament.get("name") or filament.get("material") or "Unknown"
|
|
|
|
|
+ label = f"#{sm_spool['id']} {vendor} {name}"
|
|
|
|
|
+
|
|
|
|
|
+ payload = map_spool(sm_spool)
|
|
|
|
|
+
|
|
|
|
|
+ if args.dry_run:
|
|
|
|
|
+ print(f" [DRY RUN] {label}")
|
|
|
|
|
+ for k, v in payload.items():
|
|
|
|
|
+ if v is not None:
|
|
|
|
|
+ print(f" {k}: {v}")
|
|
|
|
|
+ print()
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ try:
|
|
|
|
|
+ result = create_bambuddy_spool(args.bambuddy_url, payload, args.api_key)
|
|
|
|
|
+ print(f" Imported {label} -> Bambuddy spool #{result['id']}")
|
|
|
|
|
+ created += 1
|
|
|
|
|
+ except requests.RequestException as e:
|
|
|
|
|
+ print(f" FAILED {label}: {e}", file=sys.stderr)
|
|
|
|
|
+ failed += 1
|
|
|
|
|
+
|
|
|
|
|
+ if not args.dry_run:
|
|
|
|
|
+ print(f"\nDone: {created} imported, {failed} failed")
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ main()
|