Детализирована документация. Небольшие изменения имен параметров.
This commit is contained in:
+242
@@ -0,0 +1,242 @@
|
||||
"""Общие аргументы CLI: --config (скрипт), --root, --baseconf (конфигурация 1С)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from . import db as dbmod
|
||||
from .config import cache_dir, discover_configs, project_root_from_here, resolve_config_name
|
||||
|
||||
|
||||
class BaseconfError(Exception):
|
||||
"""Ошибка выбора конфигурации 1С (--baseconf)."""
|
||||
|
||||
def __init__(self, message: str, *, exit_code: int = 2):
|
||||
super().__init__(message)
|
||||
self.exit_code = exit_code
|
||||
|
||||
|
||||
def load_tool_config(path: Path) -> dict[str, Any]:
|
||||
"""Файл настроек утилиты index (JSON). Не путать с конфигурацией 1С."""
|
||||
try:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
except OSError as e:
|
||||
raise BaseconfError(f"Не удалось прочитать --config {path}: {e}", exit_code=1) from e
|
||||
except json.JSONDecodeError as e:
|
||||
raise BaseconfError(f"Некорректный JSON в --config {path}: {e}", exit_code=1) from e
|
||||
if not isinstance(data, dict):
|
||||
raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1)
|
||||
return data
|
||||
|
||||
|
||||
def resolve_project_root(args: argparse.Namespace) -> Path:
|
||||
"""Корень проекта: --root → project_root из --config → авто."""
|
||||
if getattr(args, "root", None):
|
||||
return Path(args.root).resolve()
|
||||
cfg_path = getattr(args, "config", None)
|
||||
if cfg_path:
|
||||
data = load_tool_config(Path(cfg_path))
|
||||
pr = data.get("project_root")
|
||||
if pr:
|
||||
return Path(pr).resolve()
|
||||
return project_root_from_here()
|
||||
|
||||
|
||||
def add_tool_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Параметры утилиты (не конфигурация 1С)."""
|
||||
parser.add_argument(
|
||||
"--config",
|
||||
"-c",
|
||||
metavar="FILE",
|
||||
help="JSON с настройками утилиты (project_root и др.), см. config.example.json",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--root",
|
||||
"-r",
|
||||
metavar="DIR",
|
||||
help="Корень проекта CRM3-26 (перекрывает project_root из --config)",
|
||||
)
|
||||
|
||||
|
||||
def add_baseconf_args(parser: argparse.ArgumentParser) -> None:
|
||||
"""Фильтр по выгрузке конфигурации 1С в репозитории."""
|
||||
parser.add_argument(
|
||||
"--baseconf",
|
||||
"-b",
|
||||
action="append",
|
||||
metavar="NAME",
|
||||
dest="baseconf",
|
||||
help=(
|
||||
"Конфигурация 1С в репозитории (папка <name>/src/). "
|
||||
"Можно несколько раз. Алиасы: target→crm3-26, source→crm3-dev. "
|
||||
"Без параметра — поиск по всем проиндексированным."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def resolve_baseconf_names(raw: list[str] | None) -> list[str]:
|
||||
"""Разрешить алиасы имён конфигураций 1С (без проверки индекса)."""
|
||||
if not raw:
|
||||
return []
|
||||
out: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for item in raw:
|
||||
for part in item.replace(";", ",").split(","):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
name = resolve_config_name(part)
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
out.append(name)
|
||||
return out
|
||||
|
||||
|
||||
def validate_baseconfs(
|
||||
project_root: Path,
|
||||
names: list[str],
|
||||
*,
|
||||
conn=None,
|
||||
require_indexed: bool = True,
|
||||
) -> list[str]:
|
||||
"""
|
||||
Проверить --baseconf: папка src/, наличие в индексе.
|
||||
Возвращает канонические имена. Иначе BaseconfError с подсказками.
|
||||
"""
|
||||
if not names:
|
||||
return []
|
||||
|
||||
resolved = resolve_baseconf_names(names)
|
||||
missing_src: list[str] = []
|
||||
for name in resolved:
|
||||
if not (project_root / name / "src").is_dir():
|
||||
missing_src.append(name)
|
||||
|
||||
if missing_src:
|
||||
available = [c.name for c in discover_configs(project_root, None)]
|
||||
lines = [
|
||||
"Конфигурация 1С не найдена в репозитории (нет каталога <name>/src/):",
|
||||
*(f" - {n}" for n in missing_src),
|
||||
"",
|
||||
"Доступные выгрузки в проекте:",
|
||||
]
|
||||
if available:
|
||||
lines.extend(f" - {n}" for n in available)
|
||||
else:
|
||||
lines.append(" (нет)")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Алиасы: target→crm3-26, source→crm3-dev, crm3_old→crm3-dev",
|
||||
]
|
||||
)
|
||||
raise BaseconfError("\n".join(lines))
|
||||
|
||||
if not require_indexed or conn is None:
|
||||
return resolved
|
||||
|
||||
indexed = {c["name"] for c in dbmod.list_configs(conn)}
|
||||
not_indexed = [n for n in resolved if n not in indexed]
|
||||
if not_indexed:
|
||||
lines = [
|
||||
"Конфигурация 1С есть в репозитории, но отсутствует в индексе cache/1c_meta/index.sqlite:",
|
||||
*(f" - {n}" for n in not_indexed),
|
||||
"",
|
||||
"Проиндексировано:",
|
||||
]
|
||||
if indexed:
|
||||
lines.extend(f" - {n}" for n in sorted(indexed))
|
||||
else:
|
||||
lines.append(" (индекс пуст)")
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
f"Переиндексация: python tools/index/index_1c.py reindex --full -b {not_indexed[0]}",
|
||||
]
|
||||
)
|
||||
raise BaseconfError("\n".join(lines))
|
||||
|
||||
return resolved
|
||||
|
||||
|
||||
def get_baseconfs_for_query(
|
||||
args: argparse.Namespace,
|
||||
conn,
|
||||
project_root: Path,
|
||||
) -> list[str] | None:
|
||||
"""None = все конфигурации в индексе; иначе список имён."""
|
||||
raw = getattr(args, "baseconf", None)
|
||||
if not raw:
|
||||
return None
|
||||
return validate_baseconfs(project_root, raw, conn=conn, require_indexed=True)
|
||||
|
||||
|
||||
def format_not_found_object(
|
||||
name: str,
|
||||
*,
|
||||
baseconfs: list[str] | None,
|
||||
conn,
|
||||
) -> str:
|
||||
"""Подсказка, если объект не найден."""
|
||||
lines = [f"Объект «{name}» не найден."]
|
||||
if baseconfs:
|
||||
lines.append(f"Ограничение --baseconf: {', '.join(baseconfs)}")
|
||||
|
||||
# похожие имена
|
||||
like = f"%{name}%"
|
||||
params: list[Any] = [like, like, like]
|
||||
sql = """
|
||||
SELECT config, full_name, synonym FROM objects
|
||||
WHERE name LIKE ? OR full_name LIKE ? OR synonym LIKE ?
|
||||
"""
|
||||
if baseconfs:
|
||||
sql += f" AND config IN ({','.join('?' * len(baseconfs))})"
|
||||
params.extend(baseconfs)
|
||||
sql += " ORDER BY config, full_name LIMIT 15"
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
if rows:
|
||||
lines.append("")
|
||||
lines.append("Возможно, имелось в виду:")
|
||||
for r in rows:
|
||||
syn = f" — {r['synonym']}" if r["synonym"] else ""
|
||||
lines.append(f" [{r['config']}] {r['full_name']}{syn}")
|
||||
else:
|
||||
indexed = [c["name"] for c in dbmod.list_configs(conn)]
|
||||
lines.append("")
|
||||
lines.append("В индексе конфигурации: " + (", ".join(indexed) if indexed else "(пусто)"))
|
||||
lines.append("Проверьте имя (Document.ЗаказКлиента) или выполните: query_1c.py search \"…\"")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def format_not_found_modules(
|
||||
query: str,
|
||||
*,
|
||||
baseconfs: list[str] | None,
|
||||
conn,
|
||||
) -> str:
|
||||
lines = [f"Модули по запросу «{query}» не найдены."]
|
||||
if baseconfs:
|
||||
lines.append(f"Ограничение --baseconf: {', '.join(baseconfs)}")
|
||||
indexed = [c["name"] for c in dbmod.list_configs(conn)]
|
||||
lines.append("Проиндексировано: " + ", ".join(indexed) if indexed else "Индекс пуст.")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def handle_baseconf_error(e: BaseconfError) -> int:
|
||||
print(str(e), file=sys.stderr)
|
||||
return e.exit_code
|
||||
|
||||
|
||||
def index_db_missing_message(project_root: Path) -> str:
|
||||
path = dbmod.global_db_path(cache_dir(project_root))
|
||||
return (
|
||||
f"Индекс не найден: {path}\n"
|
||||
"Сначала выполните:\n"
|
||||
" python tools/index/index_1c.py reindex --full -j 12\n"
|
||||
"или для одной конфигурации:\n"
|
||||
" python tools/index/index_1c.py reindex --full -b crm3-26"
|
||||
)
|
||||
Reference in New Issue
Block a user