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>
162 lines
5.8 KiB
Python
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())
|