Files
pdm/creoparams/extract.py
T
davideandClaude Opus 5 bc84407a6f Lettore di metadati per file nativi Creo Parametric
Prototipo CLI in Python che estrae parametri e proprieta' dai file .prt
senza avviare Creo e senza SDK commerciali. Solo libreria standard.

I file nativi Creo sono container con header ASCII e un indice delle
sezioni (UGC_TOC); la maggior parte delle sezioni non e' compressa e la
tabella dei parametri e' leggibile direttamente.

Ogni parametro compare in due rappresentazioni indipendenti (tabella
estesa in LargeText e copia neutra in NeuPrtSld). Il lettore le confronta
entrambe, e ne ricava:

- confidence: 0.99 quando le due copie concordano, valori inferiori se il
  dato compare una volta sola o se divergono (status "conflict");
- owner: distingue i parametri del modello da quelli delle feature, che
  vivono solo nella copia neutra.

I parametri numerici (PESO, DENSITA) usano una codifica a lunghezza
variabile non ancora risolta: vengono riportati come
"encoded_not_decoded" conservando i byte grezzi, senza inventare valori.

Validato su file PART e PART/SHEETMETAL scritti da Creo 9.0.3.0. Assiemi,
family table e versioni precedenti non sono ancora stati verificati.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 11:45:39 +02:00

162 lines
5.8 KiB
Python

"""Costruzione del record di metadati a partire da un file nativo Creo."""
from __future__ import annotations
import hashlib
import os
import re
from collections import defaultdict
from datetime import datetime, timezone
from . import params, ugc
SCHEMA_VERSION = 1
METHOD = "ugc_native_reader"
#: Prefissi dei parametri generati da Creo (impostazioni lamiera, materiale di
#: sistema, ...). Restano nell'output ma marcati come "system", per non
#: annegare i parametri aziendali nel rumore.
SYSTEM_PREFIXES = ("PTC_", "SMT_", "PRO_", "__")
#: Nome file Creo con numero di versione: "9258400201.prt.12".
_NAME_RE = re.compile(r"^(?P<stem>.+?)\.(?P<ext>prt|asm|drw|frm|lay)(?:\.(?P<ver>\d+))?$", re.I)
def split_filename(filename: str) -> tuple[str, str, int | None]:
""""9258400201.prt.12" -> ("9258400201", "prt", 12)."""
match = _NAME_RE.match(filename)
if not match:
stem, _, ext = filename.rpartition(".")
return (stem or filename), ext.lower(), None
version = match.group("ver")
return match.group("stem"), match.group("ext").lower(), int(version) if version else None
def _confidence(values: set, occurrences: int, sections: set) -> float:
if len(values) > 1:
return 0.40
if len(sections) >= 2:
return 0.99
if occurrences >= 2:
return 0.95
return 0.80
def _aggregate(occurrences: list) -> dict:
"""Raggruppa le comparse per nome e assegna stato e confidenza."""
grouped = defaultdict(list)
for occurrence in occurrences:
grouped[occurrence.name].append(occurrence)
result = {}
for name, group in grouped.items():
kind = group[0].kind
sections = {o.section for o in group if o.section}
# Solo i parametri del modello compaiono nella tabella estesa; quelli
# che vivono unicamente nella copia neutra appartengono alle feature
# (fori, lavorazioni), non al modello.
owner = "model" if any(o.form == "extended" for o in group) else "feature"
entry = {
"type": kind,
"owner": owner,
"scope": "system" if name.startswith(SYSTEM_PREFIXES) else "user",
"occurrences": len(group),
"sections": sorted(sections),
}
if kind == "real":
raws = sorted({o.raw.hex() for o in group if o.raw})
expressions = sorted({o.expression for o in group if o.expression})
entry.update(
value=None,
status="encoded_not_decoded",
confidence=None,
raw=raws[0] if len(raws) == 1 else raws,
)
if expressions:
# Il valore non e' costante: lo produce una relazione.
entry["driven_by"] = expressions[0] if len(expressions) == 1 else expressions
else:
values = {o.value for o in group}
entry["confidence"] = _confidence(values, len(group), sections)
if len(values) == 1:
entry.update(value=group[0].value, status="found")
else:
entry.update(
value=None,
status="conflict",
candidates=sorted(values),
)
result[name] = entry
return dict(sorted(result.items()))
def extract(path: str, expected: "list[str] | None" = None) -> dict:
"""Estrae i metadati da un singolo file. Solleva ugc.NotACreoFile se il
file non e' un nativo Creo riconoscibile."""
with open(path, "rb") as handle:
data = handle.read()
container = ugc.parse(data)
occurrences = params.iter_occurrences(data)
for occurrence in occurrences:
occurrence.section = container.section_at(occurrence.offset)
filename = os.path.basename(path)
stem, ext, version = split_filename(filename)
parameters = _aggregate(occurrences)
# I disegni non contengono la tabella parametri del modello: segnalarne
# l'assenza come "parametro mancante" sarebbe fuorviante.
holds_parameters = container.base_kind in ("PART", "ASSEMBLY")
if holds_parameters:
for name in expected or []:
parameters.setdefault(
name, {"value": None, "status": "parameter_not_present", "type": None}
)
return {
"schema_version": SCHEMA_VERSION,
"source_file": os.path.abspath(path),
"file": {
"name": filename,
"size": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
"modified": datetime.fromtimestamp(
os.path.getmtime(path), timezone.utc
).isoformat(),
"creo_version_number": version,
},
"model": {
"name": container.common_name.rsplit(".", 1)[0] or stem,
"file_name": container.common_name or f"{stem}.{ext}",
"kind": container.base_kind,
"subtype": container.subtype,
"creo_version": container.creo_version,
"banner": container.banner,
},
"extraction": {
"method": METHOD,
"extracted_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"sections": len(container.sections),
"parameters_found": len(occurrences),
"holds_model_parameters": holds_parameters,
},
"parameters": parameters,
}
def latest_versions(paths: "list[str]") -> "list[str]":
"""Tra piu' versioni dello stesso modello tiene solo la piu' recente."""
best: dict = {}
for path in paths:
stem, ext, version = split_filename(os.path.basename(path))
key = (os.path.dirname(os.path.abspath(path)), stem, ext)
current = best.get(key)
if current is None or (version or -1) > (current[0] or -1):
best[key] = (version, path)
return sorted(path for _, path in best.values())