Release 0.3.2: YAML config, default_baseconfs, autodiscover.

Конфиг утилиты переведён на YAML; default_baseconfs для reindex без -b;
автопоиск config.local.yml/.yaml/.json; обобщённая документация.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mihailkudravcev
2026-07-17 10:56:56 +03:00
parent 45e798f2bf
commit 404659a545
14 changed files with 293 additions and 151 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
from pathlib import Path
__version__ = "0.3.1"
__version__ = "0.3.2"
__status__ = "in development"
__author__ = "Michael BAG"
__email__ = "mk@p7net.ru"
+85 -16
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
@@ -21,25 +20,89 @@ class BaseconfError(Exception):
def load_tool_config(path: Path) -> dict[str, Any]:
"""Файл настроек утилиты index (JSON). Не путать с конфигурацией 1С."""
"""Файл настроек утилиты index (YAML; legacy JSON поддерживается)."""
ext = path.suffix.lower()
try:
data = json.loads(path.read_text(encoding="utf-8"))
raw = 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 ext == ".json":
try:
import json
data = json.loads(raw)
except Exception 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
try:
import yaml # type: ignore
except Exception as e:
raise BaseconfError(
"Для чтения --config в формате YAML нужен пакет pyyaml: pip install pyyaml",
exit_code=1,
) from e
try:
data = yaml.safe_load(raw)
except Exception as e:
raise BaseconfError(f"Некорректный YAML в --config {path}: {e}", exit_code=1) from e
if not isinstance(data, dict):
raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1)
raise BaseconfError(f"--config {path}: ожидается YAML-объект", exit_code=1)
return data
def discover_local_tool_config() -> Path | None:
"""Автопоиск локального конфига без --config."""
candidates = ("config.local.yml", "config.local.yaml", "config.local.json")
roots = [Path.cwd(), project_root_from_here()]
seen: set[Path] = set()
for root in roots:
for rel in (Path("tools/index"), Path(".")):
base = (root / rel).resolve()
if base in seen or not base.is_dir():
continue
seen.add(base)
for name in candidates:
p = base / name
if p.is_file():
return p
return None
def apply_tool_config(data: dict[str, Any]) -> None:
"""Применить настройки из YAML (--config): алиасы и дефолтные baseconf."""
from .config import set_config_aliases, set_default_baseconfs
aliases = data.get("baseconf_aliases")
if aliases is not None:
if not isinstance(aliases, dict):
raise BaseconfError("--config: baseconf_aliases должен быть объектом YAML")
set_config_aliases({str(k): str(v) for k, v in aliases.items()})
default_baseconfs = data.get("default_baseconfs")
if default_baseconfs is not None:
if not isinstance(default_baseconfs, list):
raise BaseconfError("--config: default_baseconfs должен быть YAML-списком")
set_default_baseconfs([str(x) for x in default_baseconfs])
else:
set_default_baseconfs([])
def resolve_project_root(args: argparse.Namespace) -> Path:
"""Корень проекта: --root → project_root из --config → авто."""
"""Корень проекта: --root → --config/auto-config → авто."""
if getattr(args, "root", None):
return Path(args.root).resolve()
cfg_path = getattr(args, "config", None)
if not cfg_path:
auto = discover_local_tool_config()
if auto is not None:
cfg_path = str(auto)
setattr(args, "config", cfg_path)
if cfg_path:
data = load_tool_config(Path(cfg_path))
apply_tool_config(data)
pr = data.get("project_root")
if pr:
return Path(pr).resolve()
@@ -52,13 +115,16 @@ def add_tool_args(parser: argparse.ArgumentParser) -> None:
"--config",
"-c",
metavar="FILE",
help="JSON с настройками утилиты (project_root и др.), см. config.example.json",
help=(
"YAML с настройками утилиты (project_root и др.), см. config.example.yml. "
"Без параметра: автопоиск config.local.yml/.yaml/.json"
),
)
parser.add_argument(
"--root",
"-r",
metavar="DIR",
help="Корень проекта CRM3-26 (перекрывает project_root из --config)",
help="Корень проекта с выгрузками 1С (перекрывает project_root из --config)",
)
@@ -69,7 +135,10 @@ def add_baseconf_args(
) -> None:
"""Фильтр по выгрузке конфигурации 1С в репозитории."""
if mode == "index":
default_hint = "Без параметра — индексировать все выгрузки с src/ в репозитории."
default_hint = (
"Без параметра — default_baseconfs из --config; "
"если блок не задан, индексировать все выгрузки с src/."
)
else:
default_hint = "Без параметра — поиск по всем проиндексированным."
parser.add_argument(
@@ -80,8 +149,8 @@ def add_baseconf_args(
dest="baseconf",
help=(
"Конфигурация 1С в репозитории (папка <name>/src/). "
"Можно несколько раз или через запятую: -b target,source. "
"Алиасы: target→crm3-26, source→crm3-dev. "
"Можно несколько раз или через запятую: -b cfg1,cfg2. "
"Краткие имена (алиасы) — в baseconf_aliases файла --config. "
f"{default_hint}"
),
)
@@ -192,7 +261,7 @@ def validate_baseconfs(
lines.extend(
[
"",
"Алиасы: target→crm3-26, source→crm3-dev, crm3_old→crm3-dev",
"Краткие имена (алиасы) для -b задаются в baseconf_aliases файла --config.",
]
)
raise BaseconfError("\n".join(lines))
@@ -216,7 +285,7 @@ def validate_baseconfs(
lines.extend(
[
"",
f"Переиндексация: python tools/index/index_1c.py reindex --full -b {not_indexed[0]}",
f"Переиндексация: python index_1c.py reindex --full -b {not_indexed[0]}",
]
)
raise BaseconfError("\n".join(lines))
@@ -297,7 +366,7 @@ def index_db_missing_message(project_root: Path) -> str:
return (
f"Индекс не найден: {path}\n"
"Сначала выполните:\n"
" python tools/index/index_1c.py reindex --full -j 12\n"
" python index_1c.py reindex --full -j 12\n"
"или для одной конфигурации:\n"
" python tools/index/index_1c.py reindex --full -b crm3-26"
" python index_1c.py reindex --full -b <имя-выгрузки>"
)
+47 -26
View File
@@ -5,28 +5,10 @@ from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
# Папки выгрузок относительно корня проекта (есть src/).
DEFAULT_CONFIGS: tuple[str, ...] = (
"crm3-26",
"crm3-26.rhana",
"crm3-dev",
"crm3-dev.rhana",
"crm3-dev.docs",
"ws-rhana",
"bu-corp",
"bu-corp.rhana",
"diadoc.ext.rhana",
)
# Алиасы имён конфигураций 1С для CLI (--baseconf / -b).
CONFIG_ALIASES: dict[str, str] = {
"crm3_26": "crm3-26",
"crm3_old": "crm3-dev",
"crm3-old": "crm3-dev",
"crm3_dev": "crm3-dev",
"target": "crm3-26",
"source": "crm3-dev",
}
# Алиасы имён конфигураций 1С для CLI (--baseconf / -b), задаются через --config
# (поле baseconf_aliases в YAML). См. config.example.yml.
_RUNTIME_ALIASES: dict[str, str] = {}
_RUNTIME_DEFAULT_BASECONFS: list[str] = []
# Папка EDT → канонический тип (английский идентификатор выгрузки).
OBJECT_TYPE_FOLDERS: dict[str, str] = {
@@ -192,13 +174,52 @@ class ConfigPaths:
def project_root_from_here() -> Path:
"""tools/index/index1c → корень проекта."""
return Path(__file__).resolve().parents[3]
"""Корень проекта: каталог с index_1c.py и VERSION."""
here = Path(__file__).resolve().parent
for candidate in (here.parent, here.parent.parent, here.parent.parent.parent):
if (candidate / "index_1c.py").is_file() and (candidate / "VERSION").is_file():
return candidate
return here.parent
def set_config_aliases(aliases: dict[str, str] | None) -> None:
"""Задать алиасы baseconf из файла --config (baseconf_aliases)."""
global _RUNTIME_ALIASES
_RUNTIME_ALIASES = {str(k): str(v) for k, v in (aliases or {}).items()}
def get_config_aliases() -> dict[str, str]:
return dict(_RUNTIME_ALIASES)
def set_default_baseconfs(names: list[str] | None) -> None:
"""Задать default_baseconfs из файла --config (YAML)."""
global _RUNTIME_DEFAULT_BASECONFS
_RUNTIME_DEFAULT_BASECONFS = [str(x) for x in (names or []) if str(x).strip()]
def get_default_baseconfs() -> list[str]:
"""Список baseconf по умолчанию для reindex без -b."""
return list(_RUNTIME_DEFAULT_BASECONFS)
def resolve_config_name(name: str) -> str:
key = name.strip()
return CONFIG_ALIASES.get(key, CONFIG_ALIASES.get(key.lower(), key))
aliases = get_config_aliases()
return aliases.get(key, aliases.get(key.lower(), key))
def list_config_dirs(project_root: Path) -> list[str]:
"""Все каталоги с выгрузкой 1С (<name>/src/) в корне проекта."""
if not project_root.is_dir():
return []
found: list[str] = []
for child in sorted(project_root.iterdir()):
if not child.is_dir() or child.name.startswith("."):
continue
if (child / "src").is_dir():
found.append(child.name)
return found
def resolve_object_type(name: str) -> str:
@@ -217,7 +238,7 @@ def resolve_object_type(name: str) -> str:
def discover_configs(project_root: Path, names: list[str] | None = None) -> list[ConfigPaths]:
wanted = [resolve_config_name(n) for n in names] if names else list(DEFAULT_CONFIGS)
wanted = [resolve_config_name(n) for n in names] if names else list_config_dirs(project_root)
found: list[ConfigPaths] = []
for name in wanted:
root = project_root / name
+1 -1
View File
@@ -245,7 +245,7 @@ def init_db(conn: sqlite3.Connection) -> None:
if ver > SCHEMA_VERSION:
raise RuntimeError(
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
"Обновите tools/index."
"Обновите утилиту index до более новой версии."
)
if ver < SCHEMA_VERSION:
migrate_schema(conn, ver)
+2 -2
View File
@@ -23,7 +23,7 @@ def open_index(project_root: Path, *, readonly: bool = True) -> sqlite3.Connecti
path = dbmod.global_db_path(cache_dir(project_root))
if not path.is_file():
raise FileNotFoundError(
f"Индекс не найден: {path}. Запустите: python tools/index/index_1c.py reindex --full"
f"Индекс не найден: {path}. Запустите: python index_1c.py reindex --full"
)
# мягкая миграция схемы (например 2→3: таблица register_records)
probe = dbmod.connect(path, readonly=True)
@@ -403,7 +403,7 @@ def find_refs_to(
Возвращает (строки, totals_by_config).
При поиске по нескольким конфигурациям ``limit`` максимум **на каждую**
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT).
конфигурацию (чтобы одна baseconf не вытесняла другую из-за ORDER BY + LIMIT).
"""
kind = ""
name = target
+2 -2
View File
@@ -272,8 +272,8 @@ def write_global_index_md(cache_root: Path, summary: dict[str, Any]) -> Path:
f"Режим: `{summary.get('mode', '?')}`, БД: `cache/1c_meta/index.sqlite`.",
f"Потоки: {summary.get('workers', '?')}, время: {summary.get('elapsed_sec', '?')} с.",
"",
"Поиск: `python tools/index/query_1c.py search \"\"`",
"Переиндексация: `python tools/index/index_1c.py reindex` (инкремент) / `--full`.",
"Поиск: `python query_1c.py search \"\"`",
"Переиндексация: `python index_1c.py reindex` (инкремент) / `--full`.",
"",
"## Конфигурации",
"",