feat: BOM auto-extraction and manifest field population (#276, #277)
All checks were successful
Build and Test / build (pull_request) Successful in 29m22s

Documentation updates:
- KNOWN_ISSUES.md: correct #6 (datum handling), resolve #10
  (delete_bom_entry) and #11 (silo icons), fix stale formatDate
  reference, mark completed next steps, add new next steps.
- INTEGRATION_PLAN.md: correct ztools SDK migration claim.

kc_format.py (#277):
- New _manifest_enrich_hook: populates part_uuid from SiloItemId and
  silo_instance from Silo API URL on every .kc save.
- New update_manifest_fields(): public API to update manifest fields
  in an already-saved .kc ZIP (used for post-upload revision_hash).

mods/silo submodule (#276):
- New bom_sync.py extraction engine.
- Post-commit hooks for BOM sync and manifest revision update.
- SSE bom_merged signal + Activity pane handler.
- merge_bom_json client method (forward-looking).

Refs: #276, #277
This commit is contained in:
forbes
2026-02-19 12:37:24 -06:00
parent 2ce00a527a
commit 0bc03ea421
4 changed files with 89 additions and 10 deletions

View File

@@ -45,6 +45,51 @@ def _metadata_save_hook(doc, filename, entries):
register_pre_reinject(_metadata_save_hook)
def _manifest_enrich_hook(doc, filename, entries):
"""Populate silo_instance and part_uuid from the tracked Silo object."""
raw = entries.get("silo/manifest.json")
if raw is None:
return
try:
manifest = json.loads(raw)
except (json.JSONDecodeError, ValueError):
return
changed = False
# Populate part_uuid from SiloItemId if available.
for obj in doc.Objects:
if hasattr(obj, "SiloItemId") and obj.SiloItemId:
if manifest.get("part_uuid") != obj.SiloItemId:
manifest["part_uuid"] = obj.SiloItemId
changed = True
break
# Populate silo_instance from Silo settings.
if not manifest.get("silo_instance"):
try:
import silo_commands
api_url = silo_commands._get_api_url()
if api_url:
# Strip /api suffix to get base instance URL.
instance = api_url.rstrip("/")
if instance.endswith("/api"):
instance = instance[:-4]
manifest["silo_instance"] = instance
changed = True
except Exception:
pass
if changed:
entries["silo/manifest.json"] = (json.dumps(manifest, indent=2) + "\n").encode(
"utf-8"
)
register_pre_reinject(_manifest_enrich_hook)
KC_VERSION = "1.0"
@@ -133,6 +178,34 @@ class _KcFormatObserver:
)
def update_manifest_fields(filename, updates):
"""Update fields in an existing .kc manifest after save.
*filename*: path to the .kc file.
*updates*: dict of field_name -> value to merge into the manifest.
Used by silo_commands to write ``revision_hash`` after a successful
upload (which happens after the ZIP has already been written by save).
"""
if not filename or not filename.lower().endswith(".kc"):
return
if not os.path.isfile(filename):
return
try:
with zipfile.ZipFile(filename, "a") as zf:
if "silo/manifest.json" not in zf.namelist():
return
raw = zf.read("silo/manifest.json")
manifest = json.loads(raw)
manifest.update(updates)
zf.writestr(
"silo/manifest.json",
json.dumps(manifest, indent=2) + "\n",
)
except Exception as e:
FreeCAD.Console.PrintWarning(f"kc_format: failed to update manifest: {e}\n")
def register():
"""Connect to application-level save signals."""
FreeCAD.addDocumentObserver(_KcFormatObserver())