45e798f2bf
Улучшена справка --help подкоманд (--baseconf до/после подкоманды), добавлены LICENSE (GPL v3 EN) и LICENSE.ru (справочный перевод), контакты mk@p7net.ru. Co-authored-by: Cursor <cursoragent@cursor.com>
497 lines
19 KiB
Python
Executable File
497 lines
19 KiB
Python
Executable File
#!/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,
|
||
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
|
||
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:
|
||
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): см. tools/index/config.example.json"
|
||
),
|
||
)
|
||
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())
|