Files
pdm/creoparams/cli.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

202 lines
7.9 KiB
Python

"""Interfaccia a riga di comando."""
from __future__ import annotations
import argparse
import csv
import json
import os
import sys
from . import ugc
from .extract import extract as extract_file
from .extract import latest_versions, split_filename
CREO_EXTENSIONS = (".prt", ".asm", ".drw", ".frm", ".lay")
#: Parametri aziendali attesi: se mancano vengono riportati esplicitamente
#: come "parameter_not_present" invece di sparire dall'output.
DEFAULT_EXPECTED = [
"CODICE",
"DENOMINAZIONE",
"REVISIONE",
"STAMPIGLIATURA",
"MATERIALE",
"DISEGNATORE",
"DATA",
]
def _is_creo_file(path: str) -> bool:
stem, ext, _ = split_filename(os.path.basename(path))
return f".{ext}" in CREO_EXTENSIONS
def collect(inputs: "list[str]", recursive: bool) -> "tuple[list[str], list[str]]":
"""Separa i file indicati esplicitamente da quelli trovati esplorando una
cartella: solo su questi ultimi ha senso scegliere l'ultima versione."""
explicit, discovered = [], []
for item in inputs:
if os.path.isdir(item):
walker = os.walk(item) if recursive else [(item, [], os.listdir(item))]
for root, _, files in walker:
discovered.extend(
os.path.join(root, f) for f in files if _is_creo_file(f)
)
else:
explicit.append(item)
return sorted(set(explicit)), sorted(set(discovered))
def _visible(record: dict, include_system: bool, include_features: bool = False) -> dict:
return {
name: entry
for name, entry in record["parameters"].items()
if (include_system or entry.get("scope") != "system")
and (include_features or entry.get("owner", "model") == "model")
}
def _print_table(records: "list[dict]", stream, include_system: bool = False,
include_features: bool = False) -> None:
for record in records:
model = record["model"]
header = f"{record['file']['name']} [{model['kind']}"
if model["subtype"]:
header += f"/{model['subtype']}"
header += f"] Creo {model['creo_version'] or '?'}"
print(header, file=stream)
print("-" * len(header), file=stream)
if not record["extraction"]["holds_model_parameters"]:
print(" (i disegni non contengono la tabella parametri del modello)",
file=stream)
print(file=stream)
continue
for name, entry in _visible(record, include_system, include_features).items():
status = entry.get("status")
if status == "found":
shown = entry["value"] if entry["value"] != "" else "(vuoto)"
mark = " " if entry.get("confidence", 0) >= 0.95 else "?"
elif status == "encoded_not_decoded":
shown = "<binario non decodificato>"
mark = "~"
elif status == "conflict":
shown = " | ".join(entry.get("candidates", []))
mark = "!"
else:
shown = "(assente)"
mark = "-"
print(f" {mark} {name:<22} {shown}", file=stream)
print(file=stream)
def _write_csv(records: "list[dict]", path: str, include_system: bool = False,
include_features: bool = False) -> None:
names = sorted({n for r in records for n in _visible(r, include_system, include_features)})
columns = ["file", "model", "kind", "creo_version"] + names
with open(path, "w", newline="", encoding="utf-8-sig") as handle:
writer = csv.DictWriter(handle, fieldnames=columns, delimiter=";")
writer.writeheader()
for record in records:
row = {
"file": record["file"]["name"],
"model": record["model"]["name"],
"kind": record["model"]["kind"],
"creo_version": record["model"]["creo_version"],
}
for name in names:
entry = record["parameters"].get(name, {})
row[name] = entry.get("value") if entry.get("status") == "found" else ""
writer.writerow(row)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="creoparams",
description="Estrae parametri e proprieta' dai file nativi Creo "
"(.prt/.asm/.drw) senza avviare Creo Parametric.",
epilog="Esempi:\n"
" creoparams 9258400201.prt.12 un singolo file\n"
" creoparams 9258400201.prt.12 -o - JSON su stdout\n"
" creoparams /rete/archivio -r --csv i.csv intera cartella",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("inputs", nargs="+", metavar="FILE|CARTELLA")
parser.add_argument("-o", "--json", metavar="FILE",
help="scrive il risultato in JSON; usare '-' per "
"stdout. Con un solo file emette un oggetto, "
"con piu' file una lista")
parser.add_argument("--csv", metavar="FILE",
help="scrive una tabella riepilogativa in CSV")
parser.add_argument("-r", "--recursive", action="store_true",
help="esplora le sottocartelle")
parser.add_argument("--all-versions", action="store_true",
help="elabora tutte le versioni (.prt.1, .prt.2, ...) "
"invece della sola piu' recente")
parser.add_argument("--system", action="store_true",
help="include anche i parametri generati da Creo "
"(PTC_*, SMT_*)")
parser.add_argument("--features", action="store_true",
help="include anche i parametri appartenenti alle "
"feature invece dei soli parametri di modello")
parser.add_argument("--quiet", action="store_true",
help="non stampa la tabella a video")
return parser
def main(argv: "list[str] | None" = None) -> int:
args = build_parser().parse_args(argv)
explicit, discovered = collect(args.inputs, args.recursive)
if not args.all_versions:
# Un file indicato a mano si elabora com'e': la scelta della versione
# piu' recente riguarda solo l'esplorazione di una cartella.
discovered = latest_versions(discovered)
paths = sorted(set(explicit) | set(discovered))
if not paths:
print("Nessun file Creo trovato.", file=sys.stderr)
return 1
records, failures = [], []
for path in paths:
try:
records.append(extract_file(path, DEFAULT_EXPECTED))
except ugc.NotACreoFile as error:
failures.append((path, str(error)))
except OSError as error:
failures.append((path, str(error)))
if not args.quiet:
_print_table(records, sys.stdout, args.system, args.features)
if args.json:
# Un solo file produce un oggetto, non una lista di un elemento:
# e' la forma piu' comoda da consumare a valle.
payload = records[0] if len(records) == 1 else records
if args.json == "-":
json.dump(payload, sys.stdout, indent=2, ensure_ascii=False)
sys.stdout.write("\n")
else:
with open(args.json, "w", encoding="utf-8") as handle:
json.dump(payload, handle, indent=2, ensure_ascii=False)
print(f"JSON scritto in {args.json}", file=sys.stderr)
if args.csv:
_write_csv(records, args.csv, args.system, args.features)
print(f"CSV scritto in {args.csv}", file=sys.stderr)
for path, reason in failures:
print(f"SALTATO {os.path.basename(path)}: {reason}", file=sys.stderr)
# Su un singolo file andato a buon fine il riepilogo e' solo rumore.
if len(paths) > 1 or failures:
print(
f"{len(records)} file elaborati, {len(failures)} saltati.",
file=sys.stderr,
)
return 0 if records else 2