bambuddy/scripts/import_spoolman.py

167 lines
5.8 KiB
Python
Raw Permalink Normal View History

2026-03-06 07:27:05 +01:00
#!/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()