#!/usr/bin/env python3 """Запросы к единому индексу конфигураций 1С (cache/1c_meta/index.sqlite). Параметры: --config, -c файл настроек утилиты (JSON), не конфигурация 1С --baseconf, -b выгрузка конфигурации 1С в репозитории (crm3-26, target, …) Примеры: python tools/index/query_1c.py search \"ЗаказКлиента\" python tools/index/query_1c.py search \"Партнер\" --type Document -b crm3-26 python tools/index/query_1c.py show ЗаказКлиента -b target python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26 python tools/index/query_1c.py modules \"РВС_Transfer\" -b ws-rhana python tools/index/query_1c.py refs Catalog.Партнеры -b source python tools/index/query_1c.py movements ЗаказКлиента -b target python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target python tools/index/query_1c.py object ЗаказКлиента -b target python tools/index/query_1c.py object Партнер -b crm3-26 --clear python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы """ from __future__ import annotations import argparse import json import sys from pathlib import Path HERE = Path(__file__).resolve().parent if str(HERE) not in sys.path: sys.path.insert(0, str(HERE)) from index1c.cli import ( # noqa: E402 BaseconfError, add_baseconf_args, add_tool_args, format_not_found_modules, format_not_found_object, get_baseconfs_for_query, handle_baseconf_error, index_db_missing_message, resolve_project_root, ) from index1c.config import resolve_object_type # noqa: E402 from index1c import read_version_file # noqa: E402 from index1c.search import ( # noqa: E402 find_movements, find_refs_to, get_module, get_object, open_index, search_modules, search_objects, ) from index1c.object_view import ( # noqa: E402 find_structure_matches, format_object_tree, load_object_structure, ) def _open_or_exit(root: Path): try: return open_index(root) except FileNotFoundError: print(index_db_missing_message(root), file=sys.stderr) sys.exit(1) def cmd_search(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) otype = resolve_object_type(args.type) if args.type else None rows = search_objects( conn, args.query, configs=baseconfs, object_type=otype, limit=args.limit, ) if not rows: scope = f" (baseconf: {', '.join(baseconfs)})" if baseconfs else "" print(f"Ничего не найдено{scope}.") return 2 for r in rows: syn = f" — {r['synonym']}" if r.get("synonym") else "" print(f"- [{r['config']}] `{r['full_name']}`{syn}") if r.get("snip"): print(f" {r['snip']}") return 0 finally: conn.close() def cmd_modules(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) names_only = bool(args.names_only) rows = search_modules( conn, args.query, configs=baseconfs, limit=args.limit, names_only=names_only, ) if not rows: print(format_not_found_modules(args.query, baseconfs=baseconfs, conn=conn)) return 2 for r in rows: syn = r.get("owner_synonym") or "" if names_only: # только имя модуля (owner) и синоним объекта-владельца syn_s = f" — {syn}" if syn else "" print(f"- [{r['config']}] `{r['owner']}`{syn_s}") else: syn_s = f", {syn}" if syn else "" print( f"- [{r['config']}] `{r['rel_path']}` " f"({r['module_kind']}, owner={r['owner']}{syn_s})" ) if r.get("snip"): print(f" {r['snip']}") return 0 finally: conn.close() def cmd_refs(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) rows, totals = find_refs_to( conn, args.target, configs=baseconfs, limit=args.limit ) if not rows: scope = f" в {', '.join(baseconfs)}" if baseconfs else "" print(f"Ссылок на «{args.target}» не найдено{scope}.") return 2 current_cfg = None for r in rows: if r["config"] != current_cfg: current_cfg = r["config"] total = totals.get(current_cfg, 0) shown = sum(1 for x in rows if x["config"] == current_cfg) trunc = f", показано {shown}" if shown < total else "" print(f"## [{current_cfg}] — всего ссылок: {total}{trunc}") arrow = f"{r['to_kind']}.{r['to_name']}" if r.get("is_composite"): others = r.get("other_types") or [] if others and len(others) <= 4: arrow += f" (среди прочего: {', '.join(others)})" elif others: arrow += f" (среди прочего: {', '.join(others[:3])}, … ещё {len(others) - 3})" else: arrow += " (среди прочего)" print(f"- `{r['from_full_name']}.{r['from_field']}` → {arrow}") if len(totals) > 1: grand = sum(totals.values()) shown = len(rows) print() print( f"Итого: {grand} ссылок в {len(totals)} конфигурациях; " f"выведено {shown} (лимит {args.limit} на каждую). " f"См. -b / --limit." ) return 0 finally: conn.close() def cmd_movements(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) otype = resolve_object_type(args.type) if args.type else None resolved, rows = find_movements( conn, args.target, configs=baseconfs, object_type=otype, limit=args.limit, ) if not resolved: print( format_not_found_object(args.target, baseconfs=baseconfs, conn=conn), file=sys.stderr, ) return 2 if not rows: names = ", ".join(f"`{r['full_name']}`" for r in resolved) print(f"Связей регистратор↔регистр для {names} не найдено.") return 2 # группировка: config → from_full_name → links current_cfg = None current_from = None for r in rows: if r["config"] != current_cfg: current_cfg = r["config"] current_from = None print(f"## [{current_cfg}]") if r["from_full_name"] != current_from: current_from = r["from_full_name"] syn = f" — {r['from_synonym']}" if r.get("from_synonym") else "" if r["direction"] == "document_to_registers": print(f"### Документ `{current_from}`{syn} → регистры движений") else: print(f"### Регистр `{current_from}`{syn} → регистраторы") to_syn = f" — {r['to_synonym']}" if r.get("to_synonym") else "" print(f"- `{r['to_full_name']}`{to_syn}") if len(resolved) > 1: print() print( f"Найдено объектов: {len(resolved)}. " "Уточните полное имя (Document.… / AccumulationRegister.…) или --type." ) return 0 finally: conn.close() def cmd_show(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) obj = get_object(conn, args.name, configs=baseconfs) if not obj: print(format_not_found_object(args.name, baseconfs=baseconfs, conn=conn), file=sys.stderr) return 2 print(json.dumps(obj, ensure_ascii=False, indent=2)) return 0 finally: conn.close() def cmd_object(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) otype = resolve_object_type(args.type) if args.type else None matches = find_structure_matches( conn, args.query, configs=baseconfs, object_type=otype, limit=args.limit, ) if not matches: print(format_not_found_object(args.query, baseconfs=baseconfs, conn=conn), file=sys.stderr) return 2 for i, m in enumerate(matches): if i: print() obj = load_object_structure(m) # подтянуть FillChecking/типы из свежего XML в matched_fields matched = list(m.get("matched_fields") or []) if matched and obj.get("fields"): by_f = { ( f.get("tabular_section") or "", f.get("name") or "", f.get("kind") or "", ): f for f in obj["fields"] } for mf in matched: key = ( mf.get("tabular_section") or "", mf.get("name") or "", mf.get("kind") or "", ) src = by_f.get(key) if src: mf["fill_checking"] = src.get("fill_checking") or "" mf["types"] = src.get("types") or mf.get("types") mf["synonym"] = src.get("synonym") or mf.get("synonym") mf["refs"] = src.get("refs") or mf.get("refs") # точное совпадение объекта (score 0) → clear без дерева; # иначе при найденных реквизитах — clear только по ним kind = m.get("match_kind") or "object" if args.clear and matched and m.get("score", 100) > 0: kind = "field" elif args.clear and m.get("score", 100) == 0: kind = "object" print( format_object_tree( obj, clear=bool(args.clear), matched_fields=matched, match_kind=kind, ) ) if len(matches) > 1: print() print(f"Найдено объектов: {len(matches)}. Уточните -b / --type / --limit.") return 0 finally: conn.close() def cmd_module(args: argparse.Namespace) -> int: root = resolve_project_root(args) conn = _open_or_exit(root) try: try: baseconfs = get_baseconfs_for_query(args, conn, root) except BaseconfError as e: return handle_baseconf_error(e) rows = get_module(conn, args.path, configs=baseconfs) if not rows: print(format_not_found_modules(args.path, baseconfs=baseconfs, conn=conn)) return 2 for r in rows: print(json.dumps(dict(r), ensure_ascii=False, indent=2)) return 0 finally: conn.close() def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="Поиск по cache/1c_meta/index.sqlite (метаданные и BSL-модули).", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Конфигурация 1С — параметр --baseconf (-b): папка выгрузки в репозитории.\n" "Файл настроек утилиты — --config (-c): см. tools/index/config.example.json" ), ) p.add_argument( "--version", action="version", version=f"%(prog)s {read_version_file()}", ) add_tool_args(p) sub = p.add_subparsers(dest="cmd", required=True) s = sub.add_parser("search", help="Полнотекстовый поиск объектов метаданных") s.add_argument("query", help="Имя, синоним, фрагмент поля") add_baseconf_args(s) s.add_argument("--type", help="Тип объекта: Document, Catalog, рс, …") s.add_argument("--limit", type=int, default=30) s.set_defaults(func=cmd_search) m = sub.add_parser("modules", help="Поиск по BSL: процедуры, тело модуля, путь") m.add_argument("query") add_baseconf_args(m) m.add_argument("--limit", type=int, default=30) m.add_argument( "--names-only", "-n", action="store_true", help="Только имена модулей (owner) и синонимы, без пути и фрагментов кода", ) m.set_defaults(func=cmd_modules) ref = sub.add_parser("refs", help="Обратный индекс: кто ссылается на объект") ref.add_argument("target", help="Catalog.Партнеры | CatalogRef.Партнеры | Партнеры") add_baseconf_args(ref) ref.add_argument( "--limit", type=int, default=100, help="Макс. строк на каждую конфигурацию 1С (по умолчанию 100)", ) ref.set_defaults(func=cmd_refs) mov = sub.add_parser( "movements", help="Регистратор ↔ регистры движений (RegisterRecords)", ) mov.add_argument( "target", help="Document.ЗаказКлиента | AccumulationRegister.ТоварыНаСкладах | имя", ) add_baseconf_args(mov) mov.add_argument( "--type", help="Уточнить тип: Document, AccumulationRegister, InformationRegister, …", ) mov.add_argument( "--limit", type=int, default=200, help="Макс. связей на каждый найденный объект (по умолчанию 200)", ) mov.set_defaults(func=cmd_movements) sh = sub.add_parser("show", help="Карточка объекта (JSON: поля, типы, ссылки)") sh.add_argument("name", help="ЗаказКлиента | Document.ЗаказКлиента") add_baseconf_args(sh) sh.set_defaults(func=cmd_show) ob = sub.add_parser( "object", help="Структура объекта: дерево реквизитов, обязательность, типы", ) ob.add_argument("query", help="Имя/синоним объекта или реквизита") add_baseconf_args(ob) ob.add_argument("--type", help="Ограничить типом: Document, Catalog, …") ob.add_argument("--limit", type=int, default=10, help="Макс. число объектов") ob.add_argument( "--clear", action="store_true", help=( "Краткий вывод: только найденные реквизиты; " "если совпал сам объект — только его имя без дерева" ), ) ob.set_defaults(func=cmd_object) md = sub.add_parser("module", help="Метаданные модуля по пути или owner") md.add_argument("path", help="CommonModules/… или CommonModule.Имя") add_baseconf_args(md) md.set_defaults(func=cmd_module) return p def main(argv: list[str] | None = None) -> int: parser = build_parser() args = parser.parse_args(argv) return int(args.func(args)) if __name__ == "__main__": raise SystemExit(main())