Files
index/query_1c.py
T
mihailkudravcev a9d1215820 Release 0.2.0: object tree, refs/modules options, docs
Add query object (--clear), modules -n, composite refs labels,
FillChecking/TypeSet parsing, VERSION/CHANGELOG and --version.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 16:37:41 +03:00

378 lines
14 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 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_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_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)
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())