Files
index/query_1c.py
T

489 lines
18 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 файл настроек утилиты (YAML), не конфигурация 1С
--baseconf, -b выгрузка конфигурации 1С в репозитории (имя каталога с src/)
Примеры:
python query_1c.py search \"Contract\"
python query_1c.py search \"Partner\" --type Document -b my-config
python query_1c.py show Document.Contract -b main
python query_1c.py modules \"ProcessData\" -b my-config.ext
python query_1c.py refs Catalog.Customers -b main
python query_1c.py movements Document.Contract -b my-config
python query_1c.py object Contract -b my-config --clear
python query_1c.py module CommonModules/MyModule
"""
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,
extract_baseconf_argv,
format_not_found_modules,
format_not_found_object,
get_baseconfs_for_query,
handle_baseconf_error,
index_db_missing_message,
make_parent_parser,
merge_baseconf_args,
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
enrich_matched_fields,
find_structure_matches,
format_object_tree,
load_object_structure,
load_object_structure_for_row,
)
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}")
matched = list(r.get("matched_fields") or [])
if r.get("match_kind") == "field" and matched:
obj = load_object_structure_for_row(conn, r)
enrich_matched_fields(obj, matched)
block = format_object_tree(
obj,
clear=True,
matched_fields=matched,
match_kind="field",
omit_header=True,
)
for line in block.splitlines():
if line:
print(f" {line}")
elif 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)
matched = list(m.get("matched_fields") or [])
enrich_matched_fields(obj, matched)
# точное совпадение объекта (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:
common = make_parent_parser(include_baseconf=True, baseconf_mode="query")
p = argparse.ArgumentParser(
description="Поиск по cache/1c_meta/index.sqlite (метаданные и BSL-модули).",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Конфигурация 1С — параметр --baseconf (-b): папка выгрузки в репозитории.\n"
"Можно указать до или после подкоманды; несколько раз или через запятую.\n"
"Файл настроек утилиты — --config (-c): см. config.example.yml"
),
)
p.add_argument(
"--version",
action="version",
version=f"%(prog)s {read_version_file()}",
)
add_tool_args(p)
add_baseconf_args(p, mode="query")
sub = p.add_subparsers(dest="cmd", required=True)
s = sub.add_parser(
"search",
help="Полнотекстовый поиск объектов метаданных",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Поиск объектов по имени, синониму или фрагменту поля.",
)
s.add_argument("query", help="Имя, синоним, фрагмент поля")
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: процедуры, тело модуля, путь",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Поиск по BSL-модулям: процедуры, фрагменты кода, путь файла.",
)
m.add_argument("query")
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="Обратный индекс: кто ссылается на объект",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Обратный индекс: какие реквизиты ссылаются на указанный объект.",
)
ref.add_argument("target", help="Catalog.Партнеры | CatalogRef.Партнеры | Партнеры")
ref.add_argument(
"--limit",
type=int,
default=100,
help="Макс. строк на каждую конфигурацию 1С (по умолчанию 100)",
)
ref.set_defaults(func=cmd_refs)
mov = sub.add_parser(
"movements",
help="Регистратор ↔ регистры движений (RegisterRecords)",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Связи документ ↔ регистры движений (RegisterRecords).",
)
mov.add_argument(
"target",
help="Document.ЗаказКлиента | AccumulationRegister.ТоварыНаСкладах | имя",
)
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: поля, типы, ссылки)",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Полная карточка объекта в JSON (поля, типы, ссылки).",
)
sh.add_argument("name", help="ЗаказКлиента | Document.ЗаказКлиента")
sh.set_defaults(func=cmd_show)
ob = sub.add_parser(
"object",
help="Структура объекта: дерево реквизитов, обязательность, типы",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Дерево структуры объекта: реквизиты, обязательность, типы.",
)
ob.add_argument("query", help="Имя/синоним объекта или реквизита")
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",
parents=[common],
formatter_class=argparse.RawDescriptionHelpFormatter,
description="Метаданные одного BSL-модуля по пути или имени owner.",
)
md.add_argument("path", help="CommonModules/… или CommonModule.Имя")
md.set_defaults(func=cmd_module)
return p
def main(argv: list[str] | None = None) -> int:
raw = list(argv if argv is not None else sys.argv[1:])
early_baseconf, raw = extract_baseconf_argv(raw)
parser = build_parser()
args = parser.parse_args(raw)
merge_baseconf_args(args, early_baseconf)
return int(args.func(args))
if __name__ == "__main__":
raise SystemExit(main())