Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user