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:
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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`.",
|
||||
"",
|
||||
"## Конфигурации",
|
||||
"",
|
||||
|
||||
Reference in New Issue
Block a user