Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Resolve a 1C metadata object by configurator-visible kind/name."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
KIND_ALIASES = {
|
||||
"документ": "Document",
|
||||
"documents": "Document",
|
||||
"document": "Document",
|
||||
"справочник": "Catalog",
|
||||
"catalogs": "Catalog",
|
||||
"catalog": "Catalog",
|
||||
"перечисление": "Enum",
|
||||
"enums": "Enum",
|
||||
"enum": "Enum",
|
||||
"регистрсведений": "InformationRegister",
|
||||
"регистр сведений": "InformationRegister",
|
||||
"informationregister": "InformationRegister",
|
||||
"informationregisters": "InformationRegister",
|
||||
"регистрнакопления": "AccumulationRegister",
|
||||
"регистр накопления": "AccumulationRegister",
|
||||
"accumulationregister": "AccumulationRegister",
|
||||
"accumulationregisters": "AccumulationRegister",
|
||||
"регистрбухгалтерии": "AccountingRegister",
|
||||
"регистр бухгалтерии": "AccountingRegister",
|
||||
"accountingregister": "AccountingRegister",
|
||||
"accountingregisters": "AccountingRegister",
|
||||
"отчет": "Report",
|
||||
"reports": "Report",
|
||||
"report": "Report",
|
||||
"обработка": "DataProcessor",
|
||||
"dataprocessor": "DataProcessor",
|
||||
"dataprocessors": "DataProcessor",
|
||||
"общиймодуль": "CommonModule",
|
||||
"общий модуль": "CommonModule",
|
||||
"commonmodule": "CommonModule",
|
||||
"commonmodules": "CommonModule",
|
||||
"форма": "Form",
|
||||
"forms": "Form",
|
||||
"form": "Form",
|
||||
"макет": "Template",
|
||||
"templates": "Template",
|
||||
"template": "Template",
|
||||
"команда": "Command",
|
||||
"commands": "Command",
|
||||
"command": "Command",
|
||||
}
|
||||
|
||||
|
||||
GENERATED_TYPE_KIND = {
|
||||
"DocumentObject": "Document",
|
||||
"DocumentRef": "Document",
|
||||
"DocumentManager": "Document",
|
||||
"DocumentSelection": "Document",
|
||||
"DocumentList": "Document",
|
||||
"CatalogObject": "Catalog",
|
||||
"CatalogRef": "Catalog",
|
||||
"CatalogManager": "Catalog",
|
||||
"CatalogSelection": "Catalog",
|
||||
"CatalogList": "Catalog",
|
||||
"EnumRef": "Enum",
|
||||
"EnumManager": "Enum",
|
||||
"InformationRegisterRecordSet": "InformationRegister",
|
||||
"InformationRegisterManager": "InformationRegister",
|
||||
"AccumulationRegisterRecordSet": "AccumulationRegister",
|
||||
"AccumulationRegisterManager": "AccumulationRegister",
|
||||
"AccountingRegisterRecordSet": "AccountingRegister",
|
||||
"AccountingRegisterManager": "AccountingRegister",
|
||||
"ReportManager": "Report",
|
||||
"DataProcessorManager": "DataProcessor",
|
||||
}
|
||||
|
||||
|
||||
TABLE_PREFIX_BY_ROLE = {
|
||||
"Document": "_Document",
|
||||
"DocumentChngR": "_DocumentChngR",
|
||||
"Catalog": "_Reference",
|
||||
"Reference": "_Reference",
|
||||
"Enum": "_Enum",
|
||||
"InfoRg": "_InfoRg",
|
||||
"InfoRgChngR": "_InfoRgChngR",
|
||||
"AccumRg": "_AccumRg",
|
||||
"AccumRgT": "_AccumRgT",
|
||||
"AccumRgOpt": "_AccumRgOpt",
|
||||
"AccRg": "_AccRg",
|
||||
"AccRgAT": "_AccRgAT",
|
||||
"AccRgCT": "_AccRgCT",
|
||||
"AccRgOpt": "_AccRgOpt",
|
||||
"BPr": "_BPr",
|
||||
"Task": "_Task",
|
||||
}
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
|
||||
|
||||
def decode_arg(value: str | None, encoded: str | None) -> str | None:
|
||||
if encoded:
|
||||
return base64.b64decode(encoded).decode("utf-8")
|
||||
return value
|
||||
|
||||
|
||||
def normalize(value: str | None) -> str:
|
||||
return re.sub(r"[\s._-]+", "", str(value or "")).casefold()
|
||||
|
||||
|
||||
def canonical_kind(value: str | None) -> str | None:
|
||||
if not value:
|
||||
return None
|
||||
stripped = value.strip()
|
||||
return KIND_ALIASES.get(normalize(stripped), stripped)
|
||||
|
||||
|
||||
def parse_query(kind: str | None, name: str) -> tuple[str | None, str]:
|
||||
kind = canonical_kind(kind)
|
||||
query = name.strip()
|
||||
if "." not in query:
|
||||
return kind, query
|
||||
left, right = query.split(".", 1)
|
||||
if left.startswith("cfg:"):
|
||||
left = left[4:]
|
||||
generated_kind = GENERATED_TYPE_KIND.get(left)
|
||||
if generated_kind:
|
||||
return kind or generated_kind, right
|
||||
parsed_kind = canonical_kind(left)
|
||||
return kind or parsed_kind, right
|
||||
|
||||
|
||||
def physical_name(route: dict[str, Any]) -> str | None:
|
||||
role = str(route.get("storage_role") or "")
|
||||
number = route.get("sql_number")
|
||||
if number is None:
|
||||
return None
|
||||
prefix = TABLE_PREFIX_BY_ROLE.get(role)
|
||||
if prefix:
|
||||
return f"{prefix}{number}"
|
||||
if role == "VT":
|
||||
return None
|
||||
if role == "Fld":
|
||||
return f"_Fld{number}"
|
||||
if role == "LineNo":
|
||||
return f"_LineNo{number}"
|
||||
return None
|
||||
|
||||
|
||||
def is_extension_path(relative_path: str | None, path: str | None = None) -> bool:
|
||||
relative_parts = str(relative_path or "").replace("/", "\\").split("\\")
|
||||
if relative_parts and relative_parts[0].casefold() in {"расширения", "extensions"}:
|
||||
return True
|
||||
full_parts = [part.casefold() for part in str(path or "").replace("/", "\\").split("\\")]
|
||||
return "расширения" in full_parts or "extensions" in full_parts
|
||||
|
||||
|
||||
def top_objects(item: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return item.get("xml_top_objects") or []
|
||||
|
||||
|
||||
def match_top(top: dict[str, Any], *, kind: str | None, wanted: str) -> tuple[float, str] | None:
|
||||
top_kind = canonical_kind(str(top.get("xml_kind") or ""))
|
||||
if kind and top_kind != kind:
|
||||
return None
|
||||
wanted_norm = normalize(wanted)
|
||||
name = str(top.get("name") or "")
|
||||
synonym = str(top.get("synonym") or "")
|
||||
relative_path = str(top.get("relative_path") or "")
|
||||
if normalize(name) == wanted_norm:
|
||||
return (1.0, "xml_top_object.name")
|
||||
if normalize(synonym) == wanted_norm:
|
||||
return (0.95, "xml_top_object.synonym")
|
||||
if wanted_norm and wanted_norm in normalize(name):
|
||||
return (0.82, "xml_top_object.name_contains")
|
||||
if wanted_norm and wanted_norm in normalize(synonym):
|
||||
return (0.78, "xml_top_object.synonym_contains")
|
||||
if wanted_norm and wanted_norm in normalize(relative_path):
|
||||
return (0.65, "xml_top_object.relative_path")
|
||||
return None
|
||||
|
||||
|
||||
def enrich_dbnames(routes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
for route in routes:
|
||||
copy = dict(route)
|
||||
name = physical_name(copy)
|
||||
if name:
|
||||
copy["physical_name"] = name
|
||||
result.append(copy)
|
||||
return result
|
||||
|
||||
|
||||
def compact_match(guid: str, item: dict[str, Any], top: dict[str, Any], *, score: float, match_by: str) -> dict[str, Any]:
|
||||
return {
|
||||
"score": score,
|
||||
"match_by": match_by,
|
||||
"guid": guid,
|
||||
"kind": canonical_kind(str(top.get("xml_kind") or "")),
|
||||
"name": top.get("name"),
|
||||
"synonym": top.get("synonym"),
|
||||
"source": "extension" if is_extension_path(top.get("relative_path"), top.get("path")) else "base",
|
||||
"relative_path": top.get("relative_path"),
|
||||
"path": top.get("path"),
|
||||
"route_kind": item.get("route_kind") or [],
|
||||
"storage": {
|
||||
"dbnames": enrich_dbnames(item.get("dbnames") or []),
|
||||
"config_routes": item.get("config_routes") or [],
|
||||
"extension_routes": item.get("extension_routes") or [],
|
||||
},
|
||||
"xml_occurrence_count": item.get("xml_occurrence_count"),
|
||||
}
|
||||
|
||||
|
||||
def find_matches(index: dict[str, Any], *, kind: str | None, name: str, limit: int) -> list[dict[str, Any]]:
|
||||
wanted_kind, wanted_name = parse_query(kind, name)
|
||||
matches = []
|
||||
for guid, item in (index.get("objects") or {}).items():
|
||||
for top in top_objects(item):
|
||||
match = match_top(top, kind=wanted_kind, wanted=wanted_name)
|
||||
if not match:
|
||||
continue
|
||||
score, match_by = match
|
||||
matches.append(compact_match(guid, item, top, score=score, match_by=match_by))
|
||||
matches.sort(
|
||||
key=lambda row: (
|
||||
-row["score"],
|
||||
row["source"] != "base",
|
||||
row.get("relative_path") or "",
|
||||
row.get("guid") or "",
|
||||
)
|
||||
)
|
||||
return matches[:limit]
|
||||
|
||||
|
||||
def resolve_object(index: dict[str, Any], *, kind: str | None, name: str, limit: int = 20) -> dict[str, Any]:
|
||||
wanted_kind, wanted_name = parse_query(kind, name)
|
||||
matches = find_matches(index, kind=wanted_kind, name=wanted_name, limit=limit)
|
||||
exact_base = [row for row in matches if row["score"] >= 0.95 and row["source"] == "base"]
|
||||
canonical = exact_base[0] if exact_base else (matches[0] if matches else None)
|
||||
extension_overlays = [
|
||||
row
|
||||
for row in matches
|
||||
if canonical
|
||||
and row.get("source") == "extension"
|
||||
and normalize(row.get("name")) == normalize(canonical.get("name"))
|
||||
and row.get("kind") == canonical.get("kind")
|
||||
]
|
||||
return {
|
||||
"schema": "onec_object_resolution.v1",
|
||||
"query": {"kind": wanted_kind, "name": wanted_name, "raw_kind": kind, "raw_name": name},
|
||||
"canonical": canonical,
|
||||
"extension_overlays": extension_overlays,
|
||||
"matches": matches,
|
||||
"summary": {
|
||||
"match_count": len(matches),
|
||||
"has_canonical": canonical is not None,
|
||||
"extension_overlay_count": len(extension_overlays),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Resolve 1C metadata object by visible kind/name.")
|
||||
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("--limit", type=int, default=20)
|
||||
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)
|
||||
if not name:
|
||||
raise SystemExit("Use --name or --name-b64.")
|
||||
|
||||
result = resolve_object(load_json(args.index), kind=kind, name=name, limit=args.limit)
|
||||
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), "summary": result["summary"]}, ensure_ascii=False))
|
||||
else:
|
||||
print(text)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user