"""Общие аргументы 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С в репозитории (папка /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С не найдена в репозитории (нет каталога /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" )