Детализирована документация. Небольшие изменения имен параметров.
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
"""Разбор 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()
|
||||
|
||||
# Корневой элемент объекта: <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,
|
||||
) -> list[tuple[str, Path]]:
|
||||
"""Список (object_type, xml_path) только верхний уровень src/<Folder>/*.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
|
||||
Reference in New Issue
Block a user