Release 0.3.4 with external processing indexing.

Add virtual baseconf support for external data processors so reindex can discover and index them alongside regular src-based configurations, and update docs to use generic examples without workspace-specific names.
This commit is contained in:
mihailkudravcev
2026-07-21 13:03:25 +03:00
parent 06e13395f8
commit 91521e6604
9 changed files with 195 additions and 24 deletions
+13 -2
View File
@@ -172,7 +172,13 @@ def _index_one_config(
existing = dbmod.get_file_index(conn, cfg.name) if not full else {}
seen_paths: set[str] = set()
meta_files = iter_object_xml_files(cfg.src, types=types, skip_types=skip_types)
meta_files = iter_object_xml_files(
cfg.src,
types=types,
skip_types=skip_types,
source_kind=cfg.source_kind,
external_name=cfg.external_name,
)
meta_jobs: list[tuple[str, str, str]] = []
meta_skipped = 0
for otype, path in meta_files:
@@ -222,7 +228,12 @@ def _index_one_config(
mod_skipped = 0
if index_modules:
mod_jobs: list[tuple[str, str, str]] = []
for path in iter_bsl_files(cfg.src):
bsl_paths = (
sorted((cfg.src / cfg.external_name).rglob("*.bsl"))
if cfg.source_kind == "external_processor" and cfg.external_name
else iter_bsl_files(cfg.src)
)
for path in bsl_paths:
rel = path.relative_to(cfg.src).as_posix()
seen_paths.add(rel)
mtime_ns, size = _file_stamp(path)
+12 -8
View File
@@ -137,7 +137,7 @@ def add_baseconf_args(
if mode == "index":
default_hint = (
"Без параметра — default_baseconfs из --config; "
"если блок не задан, индексировать все выгрузки с src/."
"если блок не задан, индексировать все выгрузки с src/ и внешние обработки."
)
else:
default_hint = "Без параметра — поиск по всем проиндексированным."
@@ -148,7 +148,7 @@ def add_baseconf_args(
metavar="NAME",
dest="baseconf",
help=(
"Конфигурация 1С в репозитории (папка <name>/src/). "
"Источник индексации 1С: <name>/src/ или virtual <dir>.<Обработка>. "
"Можно несколько раз или через запятую: -b cfg1,cfg2. "
"Краткие имена (алиасы) — в baseconf_aliases файла --config. "
f"{default_hint}"
@@ -234,28 +234,32 @@ def validate_baseconfs(
require_indexed: bool = True,
) -> list[str]:
"""
Проверить --baseconf: папка src/, наличие в индексе.
Проверить --baseconf: src-выгрузка или virtual внешняя обработка, наличие в индексе.
Возвращает канонические имена. Иначе BaseconfError с подсказками.
"""
if not names:
return []
resolved = resolve_baseconf_names(names)
available = {c.name for c in discover_configs(project_root, None)}
missing_src: list[str] = []
for name in resolved:
if not (project_root / name / "src").is_dir():
if discover_configs(project_root, [name]):
continue
if name not in available:
missing_src.append(name)
if missing_src:
available = [c.name for c in discover_configs(project_root, None)]
available_list = sorted(available)
lines = [
"Конфигурация 1С не найдена в репозитории (нет каталога <name>/src/):",
"Источник индексации 1С не найден в репозитории "
"(нет <name>/src/ или внешней обработки <dir>/<name>.xml + <dir>/<name>/):",
*(f" - {n}" for n in missing_src),
"",
"Доступные выгрузки в проекте:",
]
if available:
lines.extend(f" - {n}" for n in available)
if available_list:
lines.extend(f" - {n}" for n in available_list)
else:
lines.append(" (нет)")
lines.extend(
+116 -2
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
@@ -53,6 +54,7 @@ OBJECT_TYPE_FOLDERS: dict[str, str] = {
"XDTOPackages": "XDTOPackage",
"Bots": "Bot",
"ExternalDataSources": "ExternalDataSource",
"ExternalDataProcessors": "ExternalDataProcessor",
"Languages": "Language",
}
@@ -92,6 +94,10 @@ TYPE_ALIASES: dict[str, str] = {
"dataprocessors": "DataProcessor",
"обработка": "DataProcessor",
"обработки": "DataProcessor",
"externaldataprocessor": "ExternalDataProcessor",
"externaldataprocessors": "ExternalDataProcessor",
"внешняяобработка": "ExternalDataProcessor",
"внешниеобработки": "ExternalDataProcessor",
"report": "Report",
"reports": "Report",
"отчет": "Report",
@@ -171,6 +177,8 @@ class ConfigPaths:
name: str
root: Path
src: Path
source_kind: str = "src" # src | external_processor
external_name: str = ""
def project_root_from_here() -> Path:
@@ -210,7 +218,12 @@ def resolve_config_name(name: str) -> str:
def list_config_dirs(project_root: Path) -> list[str]:
"""Все каталоги с выгрузкой 1С (<name>/src/) в корне проекта."""
"""Все источники индексации в корне проекта.
Возвращает:
- обычные конфигурации/расширения: <name> (если есть <name>/src/)
- внешние обработки: <dir>.<Обработка> для каталогов вида <dir>/*.xml + <dir>/<Обработка>/
"""
if not project_root.is_dir():
return []
found: list[str] = []
@@ -219,9 +232,52 @@ def list_config_dirs(project_root: Path) -> list[str]:
continue
if (child / "src").is_dir():
found.append(child.name)
continue
if child.name.endswith(".ext"):
found.extend(_discover_external_processors(child))
return found
_SAFE_NAME_RE = re.compile(r"^[A-Za-zА-Яа-яЁё0-9_]+$")
def _looks_like_external_processor_xml(path: Path) -> bool:
try:
text = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
return False
probe = text[:8000]
return "<ExternalDataProcessor" in probe and "<MetaDataObject" in probe
def _discover_external_processor_entries(root: Path) -> list[tuple[str, str, Path]]:
"""Найти внешние обработки под root (включая вложенные папки).
Возвращает кортежи: (virtual_name, processor_name, processor_root_dir).
"""
out: list[tuple[str, str, Path]] = []
try:
xml_files = sorted(root.rglob("*.xml"))
except OSError:
return out
for xml in xml_files:
parent = xml.parent
name = xml.stem.strip()
if not name or not _SAFE_NAME_RE.match(name):
continue
obj_dir = parent / name
if not obj_dir.is_dir():
continue
if not _looks_like_external_processor_xml(xml):
continue
out.append((f"{root.name}.{name}", name, parent))
return out
def _discover_external_processors(root: Path) -> list[str]:
return [name for name, _proc, _parent in _discover_external_processor_entries(root)]
def resolve_object_type(name: str) -> str:
raw = name.strip()
if raw in FOLDER_BY_TYPE:
@@ -240,11 +296,69 @@ 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_config_dirs(project_root)
found: list[ConfigPaths] = []
seen: set[str] = set()
def _append(cfg: ConfigPaths) -> None:
if cfg.name in seen:
return
seen.add(cfg.name)
found.append(cfg)
for name in wanted:
root = project_root / name
src = root / "src"
if src.is_dir():
found.append(ConfigPaths(name=name, root=root, src=src))
_append(ConfigPaths(name=name, root=root, src=src))
continue
# virtual baseconf для внешних обработок:
# <dir>.<processor> → ищем <...>/<processor>.xml + <...>/<processor>/
if "." in name:
base, ext_name = name.rsplit(".", 1)
ext_root = project_root / base
if ext_root.is_dir():
entries = _discover_external_processor_entries(ext_root)
for _virtual_name, proc_name, proc_root in entries:
if proc_name != ext_name:
continue
_append(
ConfigPaths(
name=name,
root=proc_root,
src=proc_root,
source_kind="external_processor",
external_name=proc_name,
)
)
break
else:
# имя может уже быть каноническим virtual name
for virtual_name, proc_name, proc_root in entries:
if virtual_name != name:
continue
_append(
ConfigPaths(
name=virtual_name,
root=proc_root,
src=proc_root,
source_kind="external_processor",
external_name=proc_name,
)
)
break
if name in seen:
continue
# если передали только папку (например my-transfer.ext), развернуть все обработки
if root.is_dir():
for virtual_name, proc_name, proc_root in _discover_external_processor_entries(root):
_append(
ConfigPaths(
name=virtual_name,
root=proc_root,
src=proc_root,
source_kind="external_processor",
external_name=proc_name,
)
)
return found
+16 -1
View File
@@ -336,12 +336,27 @@ def iter_object_xml_files(
src: Path,
types: set[str] | None = None,
skip_types: set[str] | None = None,
*,
source_kind: str = "src",
external_name: str = "",
) -> list[tuple[str, Path]]:
"""Список (object_type, xml_path) только верхний уровень src/<Folder>/*.xml."""
"""Список (object_type, xml_path) для обычной выгрузки или external processing."""
skip = skip_types or set()
out: list[tuple[str, Path]] = []
if not src.is_dir():
return out
if source_kind == "external_processor":
otype = "ExternalDataProcessor"
if otype in skip:
return out
if types is not None and otype not in types:
return out
if not external_name:
return out
xml = src / f"{external_name}.xml"
if xml.is_file():
out.append((otype, xml))
return out
for folder in sorted(src.iterdir()):
if not folder.is_dir():
continue
+6
View File
@@ -53,6 +53,12 @@ def _owner_from_rel(rel: Path) -> str:
parts = rel.parts
if not parts:
return ""
# Внешняя обработка: <Обработка>/Ext/ObjectModule.bsl
if len(parts) >= 3 and parts[1] == "Ext":
return f"ExternalDataProcessor.{parts[0]}"
# Форма внешней обработки: <Обработка>/Forms/<Форма>/Ext/Form/Module.bsl
if len(parts) >= 6 and parts[1] == "Forms":
return f"ExternalDataProcessor.{parts[0]}.Form.{parts[2]}"
folder = parts[0]
otype = OBJECT_TYPE_FOLDERS.get(folder)
if otype and len(parts) >= 2: