"""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 (find_mesh, latest_versions, read_mesh, read_preview, 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, limit_texts: int = 20) -> 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 '?'}" shot = record.get("preview", {}) if shot.get("present"): header += f" anteprima {shot['width']}x{shot['height']}" print(header, file=stream) print("-" * len(header), file=stream) if not record["extraction"]["holds_model_parameters"]: sheet = record.get("drawing") if sheet and sheet["texts"]: print(" %d testi nella tavola (%d unici). I parametri del " "modello stanno nel .prt." % ( sheet["text_count"], len(sheet["texts"])), file=stream) for text in sheet["texts"][:limit_texts]: print(" %s" % text, file=stream) if len(sheet["texts"]) > limit_texts: print(" ... altri %d (usare -o per averli tutti)" % ( len(sheet["texts"]) - limit_texts), file=stream) else: 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 = "" 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 _preview_path(source: str, dest: str, single: bool) -> str: """Decide dove scrivere l'anteprima di `source`. Senza destinazione la mette accanto all'originale; se la destinazione e' una cartella ci scrive dentro; un nome esplicito vale solo con un file solo, altrimenti i file si sovrascriverebbero a vicenda. """ name = os.path.basename(source) + ".jpg" if not dest: return source + ".jpg" # La barra finale indica una cartella anche se non esiste ancora; con piu' # file la destinazione e' comunque una cartella, altrimenti si # sovrascriverebbero a vicenda. looks_like_dir = dest.endswith(("/", os.sep)) or os.path.isdir(dest) if looks_like_dir or not single: os.makedirs(dest, exist_ok=True) return os.path.join(dest, name) return dest def _save_previews(paths: "list[str]", dest: str, stream) -> None: single = len(paths) == 1 for source in paths: shot = read_preview(source) if shot is None: print(f"{os.path.basename(source)}: nessuna anteprima nel file", file=stream) continue target = _preview_path(source, dest, single) shot.save(target) print(f"Anteprima {shot.width}x{shot.height} salvata in {target}", file=stream) def _save_viewers(paths: "list[str]", dest: str, stream) -> None: from . import viewer single = len(paths) == 1 for source in paths: name = os.path.basename(source) try: mesh = read_mesh(source) except (OSError, ValueError) as error: print(f"{name}: mesh illeggibile ({error})", file=stream) continue if mesh is None: print(f"{name}: nessuna mesh accanto al file. Esportare il " f"modello in STL da Creo per vederlo in 3D.", file=stream) continue stem = split_filename(name)[0] page = viewer.build(mesh, stem, viewer.describe(mesh)) target = _preview_path(source, dest, single) target = target[:-4] + ".html" if target.endswith(".jpg") else target with open(target, "w", encoding="utf-8") as handle: handle.write(page) print("Vista 3D di %s (%d triangoli) salvata in %s" % (stem, len(mesh), target), 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("--preview", nargs="?", const="", metavar="FILE|CARTELLA", help="salva l'anteprima incorporata come JPEG. Senza " "argomento la scrive accanto al file di origine") parser.add_argument("--3d", dest="three_d", nargs="?", const="", metavar="FILE|CARTELLA", help="genera una pagina HTML per ruotare il modello " "nel browser, leggendo la mesh (.stl) esportata " "accanto al file") 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) if args.three_d is not None: _save_viewers([r["source_file"] for r in records], args.three_d, sys.stderr) if args.preview is not None: _save_previews( [r["source_file"] for r in records], args.preview, 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