Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Search within one 1C object context using configurator-visible terms."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from get_1c_form_context import attach_structure, select_form # noqa: E402
|
||||
from get_1c_object_brief_context import view_forms, view_modules # noqa: E402
|
||||
from get_1c_object_metadata import build_object_metadata # noqa: E402
|
||||
from resolve_1c_object import load_json, resolve_object # noqa: E402
|
||||
|
||||
|
||||
def decode_arg(value: str | None, encoded: str | None) -> str | None:
|
||||
if encoded:
|
||||
return base64.b64decode(encoded).decode("utf-8")
|
||||
return value
|
||||
|
||||
|
||||
def norm(value: Any) -> str:
|
||||
return "".join(ch for ch in str(value or "").casefold() if ch not in " \t\r\n._-")
|
||||
|
||||
|
||||
def text_fields(value: Any) -> list[str]:
|
||||
fields = []
|
||||
if isinstance(value, dict):
|
||||
for item in value.values():
|
||||
fields.extend(text_fields(item))
|
||||
elif isinstance(value, list):
|
||||
for item in value:
|
||||
fields.extend(text_fields(item))
|
||||
elif value is not None:
|
||||
fields.append(str(value))
|
||||
return fields
|
||||
|
||||
|
||||
def matches(query: str, payload: dict[str, Any]) -> bool:
|
||||
wanted = norm(query)
|
||||
return any(wanted in norm(text) for text in text_fields(payload))
|
||||
|
||||
|
||||
def slim(payload: dict[str, Any], keys: list[str]) -> dict[str, Any]:
|
||||
return {key: payload.get(key) for key in keys if payload.get(key) not in (None, [], "")}
|
||||
|
||||
|
||||
def add_match(result: list[dict[str, Any]], *, area: str, score: int, payload: dict[str, Any], evidence: dict[str, Any]) -> None:
|
||||
row = {"area": area, "score": score, **payload, "evidence": evidence}
|
||||
result.append(row)
|
||||
|
||||
|
||||
def search_metadata(query: str, metadata: dict[str, Any], limit: int) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
for attr in metadata.get("attributes") or []:
|
||||
payload = slim(attr, ["name", "synonym", "type", "types", "origin", "effective_action"])
|
||||
if matches(query, payload):
|
||||
add_match(result, area="metadata.attribute", score=90, payload=payload, evidence={"uuid": attr.get("uuid")})
|
||||
for section in metadata.get("tabular_sections") or []:
|
||||
payload = slim(section, ["name", "synonym", "origin"])
|
||||
if matches(query, payload):
|
||||
add_match(result, area="metadata.tabular_section", score=80, payload=payload, evidence={"uuid": section.get("uuid")})
|
||||
return result[:limit]
|
||||
|
||||
|
||||
def search_forms(query: str, forms: list[dict[str, Any]], max_form_items: int, limit: int) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
for form in attach_structure(select_form(forms, None), max_items=max_form_items):
|
||||
form_payload = slim(form, ["name", "synonym", "origin", "effective_action", "form_xml_path", "module_path"])
|
||||
if matches(query, form_payload):
|
||||
add_match(result, area="form", score=75, payload=form_payload, evidence={"uuid": form.get("uuid")})
|
||||
structures = []
|
||||
if form.get("structure"):
|
||||
structures.append((form, form.get("structure")))
|
||||
for overlay in form.get("extension_overlays") or []:
|
||||
if overlay.get("structure"):
|
||||
structures.append((overlay, overlay.get("structure")))
|
||||
for owner, structure in structures:
|
||||
origin = owner.get("origin") or {}
|
||||
for item in structure.get("items") or []:
|
||||
payload = slim(item, ["name", "kind", "title", "data_path", "command_name", "parent", "events"])
|
||||
if matches(query, payload):
|
||||
payload["form"] = owner.get("name")
|
||||
payload["origin"] = origin
|
||||
add_match(result, area="form.item", score=70, payload=payload, evidence={"id": item.get("id"), "form_xml_path": owner.get("form_xml_path")})
|
||||
for attr in structure.get("attributes") or []:
|
||||
payload = slim(attr, ["name", "title", "data_path", "type", "saved_data", "view"])
|
||||
if matches(query, payload):
|
||||
payload["form"] = owner.get("name")
|
||||
payload["origin"] = origin
|
||||
add_match(result, area="form.attribute", score=72, payload=payload, evidence={"id": attr.get("id"), "form_xml_path": owner.get("form_xml_path")})
|
||||
for command in structure.get("commands") or []:
|
||||
payload = slim(command, ["name", "title", "action", "group", "representation"])
|
||||
if matches(query, payload):
|
||||
payload["form"] = owner.get("name")
|
||||
payload["origin"] = origin
|
||||
add_match(result, area="form.command", score=74, payload=payload, evidence={"id": command.get("id"), "form_xml_path": owner.get("form_xml_path")})
|
||||
for event in structure.get("events") or []:
|
||||
payload = slim(event, ["name", "handler"])
|
||||
if matches(query, payload):
|
||||
payload["form"] = owner.get("name")
|
||||
payload["origin"] = origin
|
||||
add_match(result, area="form.event", score=76, payload=payload, evidence={"form_xml_path": owner.get("form_xml_path")})
|
||||
if len(result) >= limit:
|
||||
return result[:limit]
|
||||
return result[:limit]
|
||||
|
||||
|
||||
def line_snippet(lines: list[str], index: int, radius: int = 1) -> dict[str, Any]:
|
||||
start = max(0, index - radius)
|
||||
end = min(len(lines), index + radius + 1)
|
||||
return {
|
||||
"line": index + 1,
|
||||
"text": "".join(lines[start:end]).strip(),
|
||||
}
|
||||
|
||||
|
||||
def search_modules(query: str, modules: list[dict[str, Any]], *, search_code: bool, limit: int) -> list[dict[str, Any]]:
|
||||
result = []
|
||||
flat_modules = []
|
||||
for module in modules:
|
||||
flat_modules.append(module)
|
||||
flat_modules.extend(module.get("extension_overlays") or [])
|
||||
for module in flat_modules:
|
||||
payload = slim(module, ["name", "kind", "origin", "effective_action", "relative_path", "path", "size"])
|
||||
if matches(query, payload):
|
||||
add_match(result, area="module", score=65, payload=payload, evidence={"path": module.get("path")})
|
||||
if not search_code:
|
||||
continue
|
||||
path = Path(str(module.get("path") or ""))
|
||||
if not path.is_file():
|
||||
continue
|
||||
try:
|
||||
lines = path.read_text(encoding="utf-8-sig").splitlines(keepends=True)
|
||||
except UnicodeDecodeError:
|
||||
lines = path.read_text(encoding="cp1251", errors="replace").splitlines(keepends=True)
|
||||
wanted = norm(query)
|
||||
for index, line in enumerate(lines):
|
||||
if wanted and wanted in norm(line):
|
||||
add_match(
|
||||
result,
|
||||
area="module.code",
|
||||
score=60,
|
||||
payload=payload,
|
||||
evidence={"path": str(path), **line_snippet(lines, index)},
|
||||
)
|
||||
if len(result) >= limit:
|
||||
return result[:limit]
|
||||
return result[:limit]
|
||||
|
||||
|
||||
def build_search(
|
||||
index: dict[str, Any],
|
||||
*,
|
||||
kind: str,
|
||||
name: str,
|
||||
query: str,
|
||||
view: str,
|
||||
extension: str | None,
|
||||
search_code: bool,
|
||||
max_form_items: int,
|
||||
limit: 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}")
|
||||
metadata = build_object_metadata(index, kind=kind, name=name, view=view, extension=extension, include_storage=False)
|
||||
forms, form_counts = view_forms(canonical, resolution, view=view, extension=extension)
|
||||
modules, module_counts = view_modules(canonical, resolution, view=view, extension=extension)
|
||||
|
||||
matches_found = []
|
||||
remaining = limit
|
||||
for group in (
|
||||
search_metadata(query, metadata, remaining),
|
||||
search_forms(query, forms, max_form_items=max_form_items, limit=remaining),
|
||||
search_modules(query, modules, search_code=search_code, limit=remaining),
|
||||
):
|
||||
matches_found.extend(group)
|
||||
remaining = limit - len(matches_found)
|
||||
if remaining <= 0:
|
||||
break
|
||||
matches_found.sort(key=lambda row: row.get("score", 0), reverse=True)
|
||||
return {
|
||||
"schema": "onec_object_context_search.v1",
|
||||
"view": view,
|
||||
"extension": extension if view == "extension" else None,
|
||||
"query": {"kind": kind, "name": name, "text": query, "search_code": search_code, "max_form_items": max_form_items, "limit": limit},
|
||||
"object": {
|
||||
"kind": canonical.get("kind"),
|
||||
"name": canonical.get("name"),
|
||||
"synonym": canonical.get("synonym"),
|
||||
"uuid": canonical.get("guid"),
|
||||
},
|
||||
"matches": matches_found[:limit],
|
||||
"counts": {
|
||||
"matches": len(matches_found[:limit]),
|
||||
"forms": form_counts,
|
||||
"modules": module_counts,
|
||||
"metadata_attributes": len(metadata.get("attributes") or []),
|
||||
"metadata_tabular_sections": len(metadata.get("tabular_sections") or []),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Search one 1C object context.")
|
||||
parser.add_argument("--index", type=Path, required=True)
|
||||
parser.add_argument("--kind")
|
||||
parser.add_argument("--name")
|
||||
parser.add_argument("--text")
|
||||
parser.add_argument("--kind-b64")
|
||||
parser.add_argument("--name-b64")
|
||||
parser.add_argument("--text-b64")
|
||||
parser.add_argument("--view", choices=["effective", "base", "extension"], default="effective")
|
||||
parser.add_argument("--extension")
|
||||
parser.add_argument("--search-code", action="store_true")
|
||||
parser.add_argument("--max-form-items", type=int, default=2000)
|
||||
parser.add_argument("--limit", type=int, default=50)
|
||||
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)
|
||||
text = decode_arg(args.text, args.text_b64)
|
||||
if not kind or not name or not text:
|
||||
raise SystemExit("Use --kind/--name/--text or their --*-b64 variants.")
|
||||
result = build_search(
|
||||
load_json(args.index),
|
||||
kind=kind,
|
||||
name=name,
|
||||
query=text,
|
||||
view=args.view,
|
||||
extension=args.extension,
|
||||
search_code=args.search_code,
|
||||
max_form_items=args.max_form_items,
|
||||
limit=args.limit,
|
||||
)
|
||||
output = 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(output, encoding="utf-8")
|
||||
print(json.dumps({"output": str(args.output), "counts": result["counts"], "view": result["view"]}, ensure_ascii=False))
|
||||
else:
|
||||
print(output)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user