Files
llm/scripts/get_1c_form_context.py

493 lines
18 KiB
Python

#!/usr/bin/env python3
"""Return form structure context for a 1C object with base/effective/extension views."""
from __future__ import annotations
import argparse
import base64
import json
import sys
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parent))
from get_1c_object_code_context import extension_name_from_path # noqa: E402
from resolve_1c_object import load_json, resolve_object # noqa: E402
CONTAINER_TAGS = {"ChildItems", "Attributes", "Commands", "Events"}
STRUCTURE_TAGS = {
"AutoCommandBar",
"ButtonGroup",
"Popup",
"Button",
"UsualGroup",
"CommandBar",
"InputField",
"Table",
"TableColumn",
"Pages",
"Page",
"Decoration",
"CheckBox",
"LabelField",
}
def decode_arg(value: str | None, encoded: str | None) -> str | None:
if encoded:
return base64.b64decode(encoded).decode("utf-8")
return value
def local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def child_text(element: ET.Element, name: str) -> str | None:
for child in element:
if local_name(child.tag) == name and child.text is not None:
return child.text
return None
def ru_text(element: ET.Element, name: str) -> str | None:
child = next((item for item in element if local_name(item.tag) == name), None)
if child is None:
return None
for node in child.iter():
if local_name(node.tag) == "content" and node.text:
return node.text
return None
def event_records(element: ET.Element) -> list[dict[str, Any]]:
events = []
for container in element:
if local_name(container.tag) != "Events":
continue
for event in container:
if local_name(event.tag) == "Event":
events.append({"name": event.attrib.get("name"), "handler": event.text})
return events
def direct_named_children(element: ET.Element, container_name: str, wanted_tags: set[str] | None = None) -> list[ET.Element]:
result = []
for container in element:
if local_name(container.tag) != container_name:
continue
for child in container:
if wanted_tags is None or local_name(child.tag) in wanted_tags:
result.append(child)
return result
def element_record(element: ET.Element, *, parent: str | None, depth: int) -> dict[str, Any] | None:
tag = local_name(element.tag)
if tag not in STRUCTURE_TAGS:
return None
name = element.attrib.get("name")
record = {
"name": name,
"id": element.attrib.get("id"),
"kind": tag,
"parent": parent,
"depth": depth,
"title": ru_text(element, "Title"),
"data_path": child_text(element, "DataPath"),
"command_name": child_text(element, "CommandName"),
"events": event_records(element),
}
return {key: value for key, value in record.items() if value not in (None, [], "")}
def walk_items(element: ET.Element, *, parent: str | None = None, depth: int = 0, limit: int = 500) -> list[dict[str, Any]]:
records = []
for child in element:
tag = local_name(child.tag)
if tag in CONTAINER_TAGS:
records.extend(walk_items(child, parent=parent, depth=depth, limit=limit - len(records)))
continue
current_parent = parent
record = element_record(child, parent=parent, depth=depth)
if record:
records.append(record)
current_parent = record.get("name") or parent
if len(records) >= limit:
return records[:limit]
records.extend(walk_items(child, parent=current_parent, depth=depth + 1, limit=limit - len(records)))
if len(records) >= limit:
return records[:limit]
return records
def attribute_record(element: ET.Element) -> dict[str, Any]:
columns = [attribute_column_record(item, parent_name=element.attrib.get("name")) for item in direct_named_children(element, "Columns", {"Column"})]
record = {
"name": element.attrib.get("name"),
"id": element.attrib.get("id"),
"title": ru_text(element, "Title"),
"data_path": child_text(element, "DataPath"),
"saved_data": child_text(element, "SavedData"),
"view": child_text(element, "View"),
"columns": columns,
}
value_type = child_text(element, "Type")
if value_type:
record["type"] = value_type
return {key: value for key, value in record.items() if value not in (None, [], "")}
def attribute_column_record(element: ET.Element, *, parent_name: str | None) -> dict[str, Any]:
name = element.attrib.get("name")
record = {
"name": name,
"id": element.attrib.get("id"),
"title": ru_text(element, "Title"),
"data_path": ".".join(item for item in (parent_name, name) if item),
}
value_type = child_text(element, "Type")
if value_type:
record["type"] = value_type
return {key: value for key, value in record.items() if value not in (None, [], "")}
def command_record(element: ET.Element) -> dict[str, Any]:
record = {
"name": element.attrib.get("name"),
"id": element.attrib.get("id"),
"title": ru_text(element, "Title"),
"action": child_text(element, "Action"),
"group": child_text(element, "Group"),
"representation": child_text(element, "Representation"),
}
return {key: value for key, value in record.items() if value not in (None, [], "")}
def form_attribute_title_index(attributes: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
for attribute in attributes:
name = attribute.get("name")
if name:
result[str(name)] = {"source": "form_attribute", "record": attribute}
for column in attribute.get("columns") or []:
data_path = column.get("data_path")
if data_path:
result[str(data_path)] = {"source": "form_attribute_column", "record": column, "parent": attribute}
return result
def title_edit_targets(source: str, source_name: str | None) -> dict[str, Any]:
default_target = {
"form_item_title": "form_item",
"form_command_title": "form_command",
"form_attribute_title": "form_attribute",
"form_attribute_column_title": "form_attribute_column",
"fallback_name": "form_item",
"unresolved": "form_item",
"platform_standard_command": "form_item",
}.get(source, "form_item")
return {
"default_change_target": default_target,
"one_off_change_target": "form_item",
"source_name": source_name,
}
def resolve_item_title(
item: dict[str, Any],
*,
commands_by_name: dict[str, dict[str, Any]],
attributes_by_path: dict[str, dict[str, Any]],
) -> dict[str, Any]:
explicit_title = item.get("title")
if explicit_title:
return {
"effective_title": explicit_title,
"source": "form_item_title",
"source_name": item.get("name"),
"edit_targets": title_edit_targets("form_item_title", item.get("name")),
}
command_name = str(item.get("command_name") or "")
if command_name.startswith("Form.Command."):
name = command_name.removeprefix("Form.Command.")
command = commands_by_name.get(name) or {}
title = command.get("title") or name
source = "form_command_title" if command.get("title") else "fallback_name"
return {
"effective_title": title,
"source": source,
"source_name": name,
"edit_targets": title_edit_targets(source, name),
}
if command_name.startswith("Form.StandardCommand.") or command_name.startswith("StandardCommand."):
return {
"effective_title": command_name.rsplit(".", 1)[-1],
"source": "platform_standard_command",
"source_name": command_name,
"edit_targets": title_edit_targets("platform_standard_command", item.get("name")),
}
data_path = item.get("data_path")
if data_path:
attribute_hit = attributes_by_path.get(str(data_path))
if attribute_hit:
record = attribute_hit["record"]
title = record.get("title") or record.get("name") or str(data_path).split(".")[-1]
source = f"{attribute_hit['source']}_title" if record.get("title") else "fallback_name"
return {
"effective_title": title,
"source": source,
"source_name": record.get("data_path") or record.get("name"),
"edit_targets": title_edit_targets(source, record.get("data_path") or record.get("name")),
}
tail = str(data_path).split(".")[-1]
return {
"effective_title": tail,
"source": "fallback_name",
"source_name": data_path,
"edit_targets": title_edit_targets("fallback_name", data_path),
}
name = item.get("name")
return {
"effective_title": name,
"source": "fallback_name" if name else "unresolved",
"source_name": name,
"edit_targets": title_edit_targets("fallback_name" if name else "unresolved", name),
}
def attach_item_title_resolution(items: list[dict[str, Any]], attributes: list[dict[str, Any]], commands: list[dict[str, Any]]) -> list[dict[str, Any]]:
commands_by_name = {str(item.get("name")): item for item in commands if item.get("name")}
attributes_by_path = form_attribute_title_index(attributes)
resolved = []
for item in items:
copy = dict(item)
resolution = resolve_item_title(copy, commands_by_name=commands_by_name, attributes_by_path=attributes_by_path)
copy["effective_title"] = resolution["effective_title"]
copy["title_resolution"] = {key: value for key, value in resolution.items() if key != "effective_title"}
resolved.append(copy)
return resolved
def parse_form_xml(path: Path, *, source: str, extension_name: str | None, max_items: int) -> dict[str, Any]:
root = ET.parse(path).getroot()
attributes = [attribute_record(item) for item in direct_named_children(root, "Attributes", {"Attribute"})]
commands = [command_record(item) for item in direct_named_children(root, "Commands", {"Command"})]
items = attach_item_title_resolution(walk_items(root, limit=max_items), attributes, commands)
return {
"origin": {"layer": source, **({"extension": extension_name} if extension_name else {})},
"form_xml_path": str(path),
"size": path.stat().st_size,
"events": event_records(root),
"items": items,
"attributes": attributes,
"commands": commands,
"counts": {
"items": len(items),
"attributes": len(attributes),
"commands": len(commands),
"events": len(event_records(root)),
"items_truncated": len(items) >= max_items,
},
}
def form_meta_record(path: Path) -> dict[str, Any]:
root = ET.parse(path).getroot()
form = next((item for item in root.iter() if local_name(item.tag) == "Form"), root)
props = next((item for item in form if local_name(item.tag) == "Properties"), form)
name = child_text(props, "Name") or path.stem
return {
"name": name,
"synonym": ru_text(props, "Synonym"),
"uuid": form.attrib.get("uuid"),
"meta_xml_path": str(path),
"form_xml_path": str(path.with_suffix("") / "Ext" / "Form.xml"),
"module_path": str(path.with_suffix("") / "Ext" / "Form" / "Module.bsl"),
}
def collect_forms(owner_path: str | None, *, source: str, extension_name: str | None = None) -> list[dict[str, Any]]:
if not owner_path:
return []
owner_dir = Path(owner_path).with_suffix("")
forms_dir = owner_dir / "Forms"
if not forms_dir.is_dir():
return []
result = []
for path in sorted(forms_dir.glob("*.xml")):
record = form_meta_record(path)
record["origin"] = {"layer": source, **({"extension": extension_name} if extension_name else {})}
result.append(record)
return result
def norm(value: str | None) -> str:
return "".join(ch for ch in str(value or "").casefold() if not ch.isspace() and ch not in "._-")
def select_form(forms: list[dict[str, Any]], wanted: str | None) -> list[dict[str, Any]]:
if not wanted:
return forms
wanted_norm = norm(wanted)
return [
form
for form in forms
if norm(form.get("name")) == wanted_norm
or norm(form.get("synonym")) == wanted_norm
or wanted_norm in norm(form.get("name"))
]
def merge_effective(base_forms: list[dict[str, Any]], extension_forms: list[dict[str, Any]]) -> list[dict[str, Any]]:
result = []
by_name: dict[str, dict[str, Any]] = {}
for form in base_forms:
copy = dict(form)
copy["effective_action"] = "base"
by_name[norm(copy.get("name"))] = copy
result.append(copy)
for form in extension_forms:
copy = dict(form)
key = norm(copy.get("name"))
if key in by_name:
copy["effective_action"] = "extended"
by_name[key].setdefault("extension_overlays", []).append(copy)
else:
copy["effective_action"] = "added"
result.append(copy)
return result
def attach_structure(forms: list[dict[str, Any]], *, max_items: int) -> list[dict[str, Any]]:
result = []
for form in forms:
copy = dict(form)
form_xml = Path(str(copy.get("form_xml_path") or ""))
if form_xml.is_file():
origin = copy.get("origin") or {}
copy["structure"] = parse_form_xml(
form_xml,
source=origin.get("layer") or "base",
extension_name=origin.get("extension"),
max_items=max_items,
)
overlays = []
for overlay in copy.get("extension_overlays") or []:
overlay_copy = dict(overlay)
overlay_xml = Path(str(overlay_copy.get("form_xml_path") or ""))
if overlay_xml.is_file():
origin = overlay_copy.get("origin") or {}
overlay_copy["structure"] = parse_form_xml(
overlay_xml,
source=origin.get("layer") or "extension",
extension_name=origin.get("extension"),
max_items=max_items,
)
overlays.append(overlay_copy)
if overlays:
copy["extension_overlays"] = overlays
result.append(copy)
return result
def build_context(
index: dict[str, Any],
*,
kind: str,
name: str,
form: str | None,
view: str,
extension: str | None,
max_items: int,
) -> dict[str, Any]:
resolution = resolve_object(index, kind=kind, name=name, limit=100)
canonical = resolution.get("canonical")
if not canonical:
raise SystemExit(f"Object not found: {kind}.{name}")
base_forms = collect_forms(canonical.get("path"), source="base")
extension_forms = []
for overlay in resolution.get("extension_overlays") or []:
overlay_extension = extension_name_from_path(overlay.get("path")) or overlay.get("extension_name")
extension_forms.extend(collect_forms(overlay.get("path"), source="extension", extension_name=overlay_extension))
if view == "base":
forms = [{**item, "effective_action": "base"} for item in base_forms]
elif view == "effective":
forms = merge_effective(base_forms, extension_forms)
elif view == "extension":
if not extension:
raise SystemExit("Use --extension with --view extension.")
forms = [{**item, "effective_action": "extended_or_added"} for item in extension_forms if (item.get("origin") or {}).get("extension") == extension]
else:
raise SystemExit(f"Unsupported view: {view}")
selected = attach_structure(select_form(forms, form), max_items=max_items)
return {
"schema": "onec_form_context.v1",
"view": view,
"extension": extension if view == "extension" else None,
"object": {
"kind": canonical.get("kind"),
"name": canonical.get("name"),
"synonym": canonical.get("synonym"),
"uuid": canonical.get("guid"),
},
"query": {"form": form, "max_items": max_items},
"forms": selected,
"counts": {
"forms": len(selected),
"base_forms": len(base_forms),
"extension_forms": len(extension_forms),
},
}
def main() -> int:
parser = argparse.ArgumentParser(description="Get 1C form context.")
parser.add_argument("--index", type=Path, required=True)
parser.add_argument("--kind")
parser.add_argument("--name")
parser.add_argument("--kind-b64")
parser.add_argument("--name-b64")
parser.add_argument("--form")
parser.add_argument("--form-b64")
parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective")
parser.add_argument("--extension")
parser.add_argument("--max-items", type=int, default=500)
parser.add_argument("--output", type=Path)
args = parser.parse_args()
kind = decode_arg(args.kind, args.kind_b64)
name = decode_arg(args.name, args.name_b64)
form = decode_arg(args.form, args.form_b64)
if not kind or not name:
raise SystemExit("Use --kind/--name or --kind-b64/--name-b64.")
result = build_context(
load_json(args.index),
kind=kind,
name=name,
form=form,
view=args.view,
extension=args.extension,
max_items=args.max_items,
)
text = json.dumps(result, ensure_ascii=False, indent=2) + "\n"
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text, encoding="utf-8")
print(json.dumps({"output": str(args.output), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False))
else:
print(text)
return 0
if __name__ == "__main__":
raise SystemExit(main())