import_spoolman.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. #!/usr/bin/env python3
  2. """Import spools from Spoolman into Bambuddy inventory.
  3. Usage:
  4. python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000
  5. python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000 --api-key YOUR_KEY
  6. python scripts/import_spoolman.py --spoolman-url http://localhost:7912 --bambuddy-url http://localhost:8000 --dry-run
  7. """
  8. import argparse
  9. import sys
  10. import requests
  11. def fetch_spoolman_spools(spoolman_url: str) -> list[dict]:
  12. """Fetch all spools from Spoolman API."""
  13. url = f"{spoolman_url.rstrip('/')}/api/v1/spool"
  14. resp = requests.get(url, timeout=30)
  15. resp.raise_for_status()
  16. return resp.json()
  17. def map_spool(sm_spool: dict) -> dict:
  18. """Map a Spoolman spool to a Bambuddy SpoolCreate payload."""
  19. filament = sm_spool.get("filament") or {}
  20. vendor = filament.get("vendor") or {}
  21. material = filament.get("material") or "PLA"
  22. color_hex = filament.get("color_hex") or ""
  23. # Spoolman color_hex is 6-char (#RRGGBB or RRGGBB), Bambuddy rgba is 8-char RRGGBBAA
  24. rgba = None
  25. if color_hex:
  26. color_hex = color_hex.lstrip("#")
  27. if len(color_hex) == 6:
  28. rgba = f"{color_hex}FF"
  29. elif len(color_hex) == 8:
  30. rgba = color_hex
  31. label_weight = int(filament.get("weight") or 1000)
  32. used_weight = float(sm_spool.get("used_weight") or 0)
  33. # Filament name from Spoolman (e.g. "eSun PLA+ Black")
  34. filament_name = filament.get("name") or ""
  35. # Vendor name (e.g. "eSun", "Bambu Lab")
  36. brand = vendor.get("name")
  37. # Color name - prefer filament color name if present
  38. color_name = filament.get("color_hex_name") or None
  39. # Cost: Spoolman stores price per spool, we need cost per kg
  40. cost_per_kg = None
  41. spool_price = sm_spool.get("price") or filament.get("price")
  42. if spool_price and label_weight > 0:
  43. cost_per_kg = round(float(spool_price) / (label_weight / 1000), 2)
  44. # Temperature range from filament settings
  45. nozzle_temp_min = filament.get("settings", {}).get("nozzle_temperature_min") if filament.get("settings") else None
  46. nozzle_temp_max = filament.get("settings", {}).get("nozzle_temperature_max") if filament.get("settings") else None
  47. # Extra fields
  48. extra = sm_spool.get("extra") or {}
  49. tag_uid = extra.get("tag") or None
  50. # Build note with Spoolman reference
  51. note_parts = []
  52. if sm_spool.get("comment"):
  53. note_parts.append(sm_spool["comment"])
  54. if sm_spool.get("lot_nr"):
  55. note_parts.append(f"Lot: {sm_spool['lot_nr']}")
  56. note_parts.append(f"Imported from Spoolman (ID: {sm_spool['id']})")
  57. note = " | ".join(note_parts)
  58. payload = {
  59. "material": material,
  60. "color_name": color_name,
  61. "rgba": rgba.upper() if rgba else None,
  62. "brand": brand,
  63. "label_weight": label_weight,
  64. "weight_used": used_weight,
  65. "note": note,
  66. "cost_per_kg": cost_per_kg,
  67. "tag_uid": tag_uid,
  68. "data_origin": "spoolman",
  69. }
  70. if filament_name:
  71. payload["subtype"] = filament_name
  72. if nozzle_temp_min is not None:
  73. payload["nozzle_temp_min"] = int(nozzle_temp_min)
  74. if nozzle_temp_max is not None:
  75. payload["nozzle_temp_max"] = int(nozzle_temp_max)
  76. return payload
  77. def create_bambuddy_spool(bambuddy_url: str, spool_data: dict, api_key: str | None = None) -> dict:
  78. """Create a spool in Bambuddy inventory."""
  79. url = f"{bambuddy_url.rstrip('/')}/api/v1/inventory/spools"
  80. headers = {}
  81. if api_key:
  82. headers["X-API-Key"] = api_key
  83. resp = requests.post(url, json=spool_data, headers=headers, timeout=30)
  84. resp.raise_for_status()
  85. return resp.json()
  86. def main():
  87. parser = argparse.ArgumentParser(description="Import spools from Spoolman into Bambuddy inventory")
  88. parser.add_argument("--spoolman-url", required=True, help="Spoolman URL (e.g. http://localhost:7912)")
  89. parser.add_argument("--bambuddy-url", required=True, help="Bambuddy URL (e.g. http://localhost:8000)")
  90. parser.add_argument("--api-key", help="Bambuddy API key (required if auth is enabled)")
  91. parser.add_argument("--dry-run", action="store_true", help="Print mapped spools without importing")
  92. parser.add_argument("--archived", action="store_true", help="Include archived Spoolman spools")
  93. args = parser.parse_args()
  94. print(f"Fetching spools from {args.spoolman_url}...")
  95. try:
  96. sm_spools = fetch_spoolman_spools(args.spoolman_url)
  97. except requests.RequestException as e:
  98. print(f"Error fetching from Spoolman: {e}", file=sys.stderr)
  99. sys.exit(1)
  100. if not args.archived:
  101. sm_spools = [s for s in sm_spools if not s.get("archived")]
  102. print(f"Found {len(sm_spools)} spools in Spoolman")
  103. if not sm_spools:
  104. print("Nothing to import.")
  105. return
  106. created = 0
  107. failed = 0
  108. for sm_spool in sm_spools:
  109. filament = sm_spool.get("filament") or {}
  110. vendor = (filament.get("vendor") or {}).get("name", "?")
  111. name = filament.get("name") or filament.get("material") or "Unknown"
  112. label = f"#{sm_spool['id']} {vendor} {name}"
  113. payload = map_spool(sm_spool)
  114. if args.dry_run:
  115. print(f" [DRY RUN] {label}")
  116. for k, v in payload.items():
  117. if v is not None:
  118. print(f" {k}: {v}")
  119. print()
  120. continue
  121. try:
  122. result = create_bambuddy_spool(args.bambuddy_url, payload, args.api_key)
  123. print(f" Imported {label} -> Bambuddy spool #{result['id']}")
  124. created += 1
  125. except requests.RequestException as e:
  126. print(f" FAILED {label}: {e}", file=sys.stderr)
  127. failed += 1
  128. if not args.dry_run:
  129. print(f"\nDone: {created} imported, {failed} failed")
  130. if __name__ == "__main__":
  131. main()