Files
mihailkudravcev 91521e6604 Release 0.3.4 with external processing indexing.
Add virtual baseconf support for external data processors so reindex can discover and index them alongside regular src-based configurations, and update docs to use generic examples without workspace-specific names.
2026-07-21 13:03:25 +03:00

373 lines
11 KiB
Python

"""Разбор 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)
for t in typ.findall(f".//{V8}TypeSet"):
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 = ""
fill_checking: str = "" # ShowError | DontCheck | …
@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,
fill_checking=child_text(props, "FillChecking"),
)
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()
# Корневой элемент объекта: <Catalog>, <Document>, ...
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,
*,
source_kind: str = "src",
external_name: str = "",
) -> list[tuple[str, Path]]:
"""Список (object_type, xml_path) для обычной выгрузки или external processing."""
skip = skip_types or set()
out: list[tuple[str, Path]] = []
if not src.is_dir():
return out
if source_kind == "external_processor":
otype = "ExternalDataProcessor"
if otype in skip:
return out
if types is not None and otype not in types:
return out
if not external_name:
return out
xml = src / f"{external_name}.xml"
if xml.is_file():
out.append((otype, xml))
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