"""Разбор XML метаданных объектов 1С (выгрузка EDT).""" from __future__ import annotations import re import xml.etree.ElementTree as ET from dataclasses import asdict, dataclass, field from pathlib import Path from typing import Any, Iterable from .config import FIELD_RICH_TYPES, OBJECT_TYPE_FOLDERS MD = "{http://v8.1c.ru/8.3/MDClasses}" V8 = "{http://v8.1c.ru/8.1/data/core}" REF_TYPE_RE = re.compile( r"^cfg:(CatalogRef|DocumentRef|EnumRef|ChartOfCharacteristicTypesRef|" r"ChartOfAccountsRef|ChartOfCalculationTypesRef|ExchangePlanRef|" r"BusinessProcessRef|TaskRef|DefinedType)\.(.+)$" ) FIELD_TAGS = frozenset( { "Attribute", "Dimension", "Resource", "TabularSection", "Column", "EnumValue", "AddressingAttribute", } ) def local(tag: str) -> str: return tag.split("}")[-1] if "}" in tag else tag def text_of(el: ET.Element | None) -> str: if el is None: return "" return (el.text or "").strip() def child_text(props: ET.Element, tag: str) -> str: node = props.find(f"{MD}{tag}") if node is None: # без namespace — на всякий случай for ch in props: if local(ch.tag) == tag: return (ch.text or "").strip() return "" return (node.text or "").strip() def collect_localized(props: ET.Element, tag: str) -> dict[str, str]: """Синоним / подсказка: lang → content.""" node = props.find(f"{MD}{tag}") if node is None: return {} out: dict[str, str] = {} for item in node.findall(f"{V8}item"): lang = text_of(item.find(f"{V8}lang")) or "ru" content = text_of(item.find(f"{V8}content")) if content: out[lang] = content return out def synonym_str(loc: dict[str, str]) -> str: if not loc: return "" if "ru" in loc: parts = [loc["ru"]] + [f"{k}:{v}" for k, v in loc.items() if k != "ru"] return " | ".join(parts) return " | ".join(f"{k}:{v}" for k, v in loc.items()) def collect_types(props: ET.Element) -> list[str]: typ = props.find(f"{MD}Type") if typ is None: return [] types: list[str] = [] for t in typ.findall(f".//{V8}Type"): raw = (t.text or "").strip() if raw: types.append(raw) return types def parse_ref_targets(types: Iterable[str]) -> list[dict[str, str]]: refs: list[dict[str, str]] = [] for t in types: m = REF_TYPE_RE.match(t) if m: refs.append({"kind": m.group(1), "name": m.group(2), "raw": t}) elif t.startswith("cfg:") and "Ref." in t: # прочие cfg:*Ref.* body = t[4:] if "." in body: kind, name = body.split(".", 1) refs.append({"kind": kind, "name": name, "raw": t}) return refs @dataclass class FieldRec: kind: str # Attribute | Dimension | Resource | Column | EnumValue | ... name: str synonym: str = "" comment: str = "" tooltip: str = "" types: list[str] = field(default_factory=list) refs: list[dict[str, str]] = field(default_factory=list) tabular_section: str = "" @dataclass class ObjectRec: config: str object_type: str name: str synonym: str = "" comment: str = "" tooltip: str = "" explanation: str = "" uuid: str = "" path: str = "" fields: list[FieldRec] = field(default_factory=list) owners: list[str] = field(default_factory=list) register_records: list[str] = field(default_factory=list) based_on: list[str] = field(default_factory=list) input_by_string: list[str] = field(default_factory=list) @property def full_name(self) -> str: return f"{self.object_type}.{self.name}" def search_text(self) -> str: parts = [ self.object_type, self.name, self.synonym, self.comment, self.tooltip, self.explanation, ] for f in self.fields: parts.extend([f.kind, f.name, f.synonym, f.comment, f.tooltip, f.tabular_section]) parts.extend(f.types) parts.extend(self.owners) parts.extend(self.register_records) parts.extend(self.based_on) return "\n".join(p for p in parts if p) def to_dict(self) -> dict[str, Any]: d = asdict(self) d["full_name"] = self.full_name return d def _parse_field_props(props: ET.Element, kind: str, tabular: str = "") -> FieldRec | None: name = child_text(props, "Name") if not name: return None types = collect_types(props) return FieldRec( kind=kind, name=name, synonym=synonym_str(collect_localized(props, "Synonym")), comment=child_text(props, "Comment"), tooltip=synonym_str(collect_localized(props, "ToolTip")), types=types, refs=parse_ref_targets(types), tabular_section=tabular, ) def _walk_child_objects(co: ET.Element | None, object_type: str) -> list[FieldRec]: if co is None or object_type not in FIELD_RICH_TYPES: return [] fields: list[FieldRec] = [] for child in co: tag = local(child.tag) if tag == "TabularSection": ts_props = child.find(f"{MD}Properties") ts_name = child_text(ts_props, "Name") if ts_props is not None else "" if ts_props is not None and ts_name: fields.append( FieldRec( kind="TabularSection", name=ts_name, synonym=synonym_str(collect_localized(ts_props, "Synonym")), comment=child_text(ts_props, "Comment"), ) ) inner = child.find(f"{MD}ChildObjects") if inner is not None: for col in inner: if local(col.tag) != "Attribute": continue cprops = col.find(f"{MD}Properties") if cprops is None: continue rec = _parse_field_props(cprops, "Column", tabular=ts_name) if rec: fields.append(rec) continue if tag not in FIELD_TAGS: continue props = child.find(f"{MD}Properties") if props is None: continue rec = _parse_field_props(props, tag) if rec: fields.append(rec) return fields _META_PREFIXES = ( "Catalog.", "Document.", "InformationRegister.", "AccumulationRegister.", "Enum.", "Constant.", "ChartOfCharacteristicTypes.", "ExchangePlan.", "BusinessProcess.", "Task.", "DocumentJournal.", ) def _list_from_props(props: ET.Element, tag: str) -> list[str]: node = props.find(f"{MD}{tag}") if node is None: return [] items: list[str] = [] for el in node.iter(): t = (el.text or "").strip() if not t or "." not in t: continue if any(t.startswith(p) for p in _META_PREFIXES): items.append(t) seen: set[str] = set() out: list[str] = [] for x in items: if x not in seen: seen.add(x) out.append(x) return out def parse_metadata_xml( path: Path, *, config: str, object_type: str, ) -> ObjectRec | None: try: tree = ET.parse(path) except ET.ParseError: return None root = tree.getroot() # Корневой элемент объекта: , , ... obj_el: ET.Element | None = None for ch in root: if local(ch.tag) == object_type: obj_el = ch break if obj_el is None: for ch in root: if ch.find(f"{MD}Properties") is not None or ch.find("Properties") is not None: obj_el = ch break if obj_el is None: return None props = obj_el.find(f"{MD}Properties") if props is None: props = obj_el.find("Properties") if props is None: return None name = child_text(props, "Name") if not name: return None uuid = obj_el.attrib.get("uuid", "") fields = _walk_child_objects(obj_el.find(f"{MD}ChildObjects"), object_type) # Для констант / определяемых типов тип лежит в Properties if object_type in {"Constant", "DefinedType", "CommonAttribute", "SessionParameter"} and not fields: types = collect_types(props) if types: fields.append( FieldRec( kind="ValueType", name="Type", types=types, refs=parse_ref_targets(types), ) ) return ObjectRec( config=config, object_type=object_type, name=name, synonym=synonym_str(collect_localized(props, "Synonym")), comment=child_text(props, "Comment"), tooltip=synonym_str(collect_localized(props, "ToolTip")), explanation=synonym_str(collect_localized(props, "Explanation")), uuid=uuid, path=str(path), fields=fields, owners=_list_from_props(props, "Owners"), register_records=_list_from_props(props, "RegisterRecords"), based_on=_list_from_props(props, "BasedOn"), input_by_string=_list_from_props(props, "InputByString"), ) def iter_object_xml_files( src: Path, types: set[str] | None = None, skip_types: set[str] | None = None, ) -> list[tuple[str, Path]]: """Список (object_type, xml_path) только верхний уровень src//*.xml.""" skip = skip_types or set() out: list[tuple[str, Path]] = [] if not src.is_dir(): return out for folder in sorted(src.iterdir()): if not folder.is_dir(): continue otype = OBJECT_TYPE_FOLDERS.get(folder.name) if otype is None: continue if otype in skip: continue if types is not None and otype not in types: continue for xml in sorted(folder.glob("*.xml")): out.append((otype, xml)) return out