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>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user