from __future__ import annotations import argparse import json import os from collections import Counter, defaultdict from pathlib import Path from typing import Any from urllib import request def rpc(adapter_url: str, method: str, payload: dict[str, Any], *, timeout: int) -> dict[str, Any]: body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8") headers = {"Content-Type": "application/json; charset=utf-8"} service_token = str(os.getenv("ONEC_ADAPTER_SERVICE_TOKEN") or "").strip() if service_token: headers["Authorization"] = f"Bearer {service_token}" req = request.Request( f"{adapter_url.rstrip('/')}/rpc", data=body, headers=headers, method="POST", ) with request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read().decode("utf-8", errors="replace")) if not isinstance(data, dict): raise ValueError(f"{method} returned non-object response") return data def read_json(path: Path) -> dict[str, Any]: data = json.loads(path.read_text(encoding="utf-8-sig")) if not isinstance(data, dict): raise ValueError(f"{path} must contain a JSON object") return data def write_json(path: Path, data: dict[str, Any]) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def semantic_coverage(row: dict[str, Any]) -> dict[str, Any]: semantic = row.get("semantic") if isinstance(row.get("semantic"), dict) else {} coverage = semantic.get("coverage") if isinstance(semantic.get("coverage"), dict) else {} return { "mapped": int(coverage.get("mapped") or 0), "unmapped": int(coverage.get("unmapped") or 0), "total": int(coverage.get("total") or 0), "status": coverage.get("status") or "unknown", } def unmapped_parameters(row: dict[str, Any], *, limit: int) -> list[dict[str, Any]]: semantic = row.get("semantic") if isinstance(row.get("semantic"), dict) else {} params = semantic.get("unmapped_parameters") if isinstance(semantic.get("unmapped_parameters"), list) else [] result = [] for param in params: if not isinstance(param, dict): continue result.append( { "index": param.get("index"), "presentation": param.get("presentation"), "value_kind": param.get("value_kind"), "value": param.get("value"), "kind": param.get("kind"), "items": param.get("items"), } ) if len(result) >= limit: break return result def row_identity(row: dict[str, Any]) -> dict[str, Any]: return { "section": row.get("_profile_section"), "name": row.get("name"), "id": row.get("id"), "path": row.get("path"), "marker": row.get("marker"), "marker_name": row.get("marker_name"), "type_name": row.get("type_name"), "title": row.get("title"), "path_to_data": row.get("path_to_data"), } def scalar_parameters(row: dict[str, Any], *, limit: int) -> list[dict[str, Any]]: params = row.get("parameters") if isinstance(row.get("parameters"), list) else [] result = [] for param in params: if not isinstance(param, dict) or "value" not in param: continue result.append( { "index": param.get("index"), "presentation": param.get("presentation"), "value": param.get("value"), "value_kind": param.get("value_kind"), "position": param.get("position"), } ) if len(result) >= limit: break return result def iter_profile_rows(profile: dict[str, Any]) -> list[dict[str, Any]]: rows: list[dict[str, Any]] = [] for section in ("items", "attributes", "commands", "tables", "command_bars"): for row in profile.get(section) or []: if isinstance(row, dict): rows.append({**row, "_profile_section": section}) return rows def summarize_form(form: dict[str, Any], *, sample_limit: int) -> dict[str, Any]: profile = form.get("profile") if isinstance(form.get("profile"), dict) else {} rows = iter_profile_rows(profile) coverage_total = Counter() status_counts = Counter() marker_counts = Counter() type_counts = Counter() gap_rows = [] scalar_rows = [] dynamic_list_settings = [] for row in rows: coverage = semantic_coverage(row) coverage_total.update({key: coverage[key] for key in ("mapped", "unmapped", "total")}) status_counts[str(coverage.get("status") or "unknown")] += 1 if row.get("marker_name"): marker_counts[str(row.get("marker_name"))] += 1 if row.get("type_name"): type_counts[str(row.get("type_name"))] += 1 if row.get("dynamic_list_settings"): dynamic_list_settings.append({**row_identity(row), "settings": row.get("dynamic_list_settings")}) unmapped = unmapped_parameters(row, limit=sample_limit) if unmapped: gap_rows.append({**row_identity(row), "coverage": coverage, "unmapped_parameters": unmapped}) scalars = scalar_parameters(row, limit=sample_limit) if scalars: scalar_rows.append({**row_identity(row), "scalars": scalars}) return { "name": form.get("name"), "guid": form.get("guid"), "synonyms": form.get("synonyms"), "source": form.get("source"), "root": profile.get("root"), "status": profile.get("status"), "counts": { **(form.get("counts") or {}), **(profile.get("counts") or {}), **(profile.get("properties") or {}), "profile_rows": len(rows), }, "capabilities": profile.get("capabilities") or {}, "semantic_coverage": { "mapped": coverage_total["mapped"], "unmapped": coverage_total["unmapped"], "total": coverage_total["total"], "status_counts": dict(status_counts), }, "marker_counts": dict(marker_counts), "type_counts": dict(type_counts), "gap_rows_sample": gap_rows[:sample_limit], "scalar_rows_sample": scalar_rows[:sample_limit], "dynamic_list_settings": dynamic_list_settings[:sample_limit], "handler_links": profile.get("handler_links") or [], "command_links": profile.get("command_links") or [], "button_command_links": profile.get("button_command_links") or [], } def build_profile(details: dict[str, Any], *, sample_limit: int) -> dict[str, Any]: forms = [form for form in details.get("forms") or [] if isinstance(form, dict)] form_summaries = [summarize_form(form, sample_limit=sample_limit) for form in forms] totals = Counter() for form in form_summaries: coverage = form.get("semantic_coverage") or {} totals.update({key: int(coverage.get(key) or 0) for key in ("mapped", "unmapped", "total")}) by_status: defaultdict[str, int] = defaultdict(int) for form in form_summaries: for status, count in ((form.get("semantic_coverage") or {}).get("status_counts") or {}).items(): by_status[str(status)] += int(count or 0) return { "schema": "onec_form_decoder_profile.v1", "status": details.get("status"), "base_id": details.get("base_id"), "source": details.get("source"), "object": details.get("object"), "counts": { **(details.get("counts") or {}), "forms_profiled": len(form_summaries), "semantic_mapped": totals["mapped"], "semantic_unmapped": totals["unmapped"], "semantic_total": totals["total"], "semantic_status_counts": dict(sorted(by_status.items())), }, "forms": form_summaries, } def markdown_table_row(values: list[Any]) -> str: return "| " + " | ".join(str(value).replace("\n", " ") for value in values) + " |" def render_markdown(profile: dict[str, Any]) -> str: lines = ["# 1C Form Decoder Profile", ""] obj = profile.get("object") or {} lines.append(f"- Base: `{profile.get('base_id')}`") lines.append(f"- Object: `{obj.get('kind')}.{obj.get('name')}`") counts = profile.get("counts") or {} lines.append(f"- Forms profiled: `{counts.get('forms_profiled')}`") lines.append(f"- Semantic coverage: `{counts.get('semantic_mapped')}/{counts.get('semantic_total')}` mapped, `{counts.get('semantic_unmapped')}` unmapped") lines.append("") lines.append(markdown_table_row(["Form", "Status", "Rows", "Mapped", "Unmapped", "Top markers"])) lines.append(markdown_table_row(["---", "---", "---:", "---:", "---:", "---"])) for form in profile.get("forms") or []: coverage = form.get("semantic_coverage") or {} top_markers = ", ".join(f"{name}:{count}" for name, count in list((form.get("marker_counts") or {}).items())[:5]) form_counts = form.get("counts") or {} lines.append( markdown_table_row( [ form.get("name"), form.get("status"), form_counts.get("profile_rows"), coverage.get("mapped"), coverage.get("unmapped"), top_markers, ] ) ) lines.append("") for form in profile.get("forms") or []: gaps = form.get("gap_rows_sample") or [] if not gaps: continue lines.append(f"## {form.get('name')} Gap Sample") lines.append("") lines.append(markdown_table_row(["Section", "Name", "Type", "Coverage", "Unmapped sample"])) lines.append(markdown_table_row(["---", "---", "---", "---", "---"])) for row in gaps[:10]: coverage = row.get("coverage") or {} sample = ", ".join( f"{param.get('index')}:{param.get('presentation')}={param.get('value')!r}" for param in (row.get("unmapped_parameters") or [])[:6] ) lines.append( markdown_table_row( [ row.get("section"), row.get("name"), row.get("type_name") or row.get("marker_name"), f"{coverage.get('mapped')}/{coverage.get('total')}", sample, ] ) ) lines.append("") return "\n".join(lines).rstrip() + "\n" def main() -> int: parser = argparse.ArgumentParser(description="Profile decoded 1C managed forms and highlight decoder gaps.") parser.add_argument("--input-json", type=Path, help="Existing metadata.object.form.details JSON.") parser.add_argument("--adapter-url", default="http://docker.cin.su:8011") parser.add_argument("--base-id", default="upo_test") parser.add_argument("--kind") parser.add_argument("--name") parser.add_argument("--guid") parser.add_argument("--table") parser.add_argument("--form") parser.add_argument("--timeout", type=int, default=120) parser.add_argument("--sample-limit", type=int, default=20) parser.add_argument("--raw-output-json", type=Path, help="Write raw metadata.object.form.details response.") parser.add_argument("--output-json", type=Path, required=True) parser.add_argument("--output-markdown", type=Path) args = parser.parse_args() if args.input_json: details = read_json(args.input_json) else: payload: dict[str, Any] = { "base_id": args.base_id, "include_parameters": True, "max_items": 5000, "max_parameters": 500, "include_storage": True, "timeout_seconds": args.timeout, } for key in ("kind", "name", "guid", "table", "form"): value = getattr(args, key) if value: payload[key] = value details = rpc(args.adapter_url, "metadata.object.form.details", payload, timeout=args.timeout) if args.raw_output_json: write_json(args.raw_output_json, details) profile = build_profile(details, sample_limit=args.sample_limit) write_json(args.output_json, profile) if args.output_markdown: args.output_markdown.parent.mkdir(parents=True, exist_ok=True) args.output_markdown.write_text(render_markdown(profile), encoding="utf-8") print( json.dumps( { "schema": "onec_form_decoder_profile_cli_summary.v1", "status": profile.get("status"), "object": { "kind": (profile.get("object") or {}).get("kind"), "name": (profile.get("object") or {}).get("name"), }, "counts": profile.get("counts"), "output_json": str(args.output_json), "output_markdown": str(args.output_markdown) if args.output_markdown else None, }, ensure_ascii=False, indent=2, ) ) return 0 if __name__ == "__main__": raise SystemExit(main())