From 91521e66041b76658a30a7215f17d4b4d7f97f89 Mon Sep 17 00:00:00 2001 From: mihailkudravcev Date: Tue, 21 Jul 2026 13:03:25 +0300 Subject: [PATCH] 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. --- CHANGELOG.md | 16 ++++++ README.md | 14 +++-- VERSION | 2 +- index1c/build.py | 15 ++++- index1c/cli.py | 20 ++++--- index1c/config.py | 118 ++++++++++++++++++++++++++++++++++++++- index1c/parse_meta.py | 17 +++++- index1c/parse_modules.py | 6 ++ index_1c.py | 11 ++-- 9 files changed, 195 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80cbfbc..08b737a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Версионирование: [Semantic Versioning](https://semver.org/lang/ru/). +## [0.3.4] - 2026-07-21 + +### Added + +- Поддержка внешних обработок как virtual `--baseconf`: можно указать папку (например `my-transfer.ext`), и `reindex` автоматически развернёт её в `my-transfer.ext.<ИмяОбработки>` для каждой найденной пары `<Имя>.xml + <Имя>/`. + +### Changed + +- Проверка `--baseconf` и автоскан `list/reindex` теперь учитывают не только `/src/`, но и внешние обработки. +- Индексация metadata/modules для virtual external baseconf: + - metadata: `/.xml`, + - модули: `*.bsl` только внутри `//`. +- Определение owner для модулей внешней обработки: + - `ExternalDataProcessor.<ИмяОбработки>` для `Ext/ObjectModule.bsl`, + - `ExternalDataProcessor.<ИмяОбработки>.Form.<ИмяФормы>` для модулей форм. + ## [0.3.3] - 2026-07-17 ### Changed diff --git a/README.md b/README.md index a2624fb..fe679a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Индекс метаданных и модулей конфигураций 1С -**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.3**) · [CHANGELOG](CHANGELOG.md) +**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.4**) · [CHANGELOG](CHANGELOG.md) **Автор:** Michael BAG · [mk@p7net.ru](mailto:mk@p7net.ru) **Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru) @@ -42,7 +42,7 @@ python query_1c.py --version |----------|----------|------------| | `--config` | `-c` | Файл **настроек утилиты** (YAML). **Не** конфигурация 1С. См. [`config.example.yml`](config.example.yml). | | `--root` | `-r` | Корень проекта с выгрузками 1С (если запуск не из дерева проекта). | -| `--baseconf` | `-b` | Выгрузка конфигурации 1С: папка `/src/` в репозитории. Можно несколько раз. | +| `--baseconf` | `-b` | Источник индексации: `/src/` или virtual внешняя обработка `.<ИмяОбработки>`. Можно несколько раз. | | `--version` | | Версия утилиты. | Приоритет корня проекта: `--root` > `project_root` в `--config` > авто (каталог с `index_1c.py` и `VERSION`). @@ -63,7 +63,7 @@ baseconf_aliases: После этого `-b main` эквивалентно `-b my-config`. Если в YAML задан блок `default_baseconfs`, то `reindex` без `-b` использует этот список. -Если блока нет — `reindex` без `-b` индексирует **все** каталоги с `src/` в корне проекта. +Если блока нет — `reindex` без `-b` индексирует **все** каталоги с `src/` и найденные внешние обработки. `query_1c.py` без `-b` ищет по **всем** конфигурациям в индексе. Список выгрузок: `python index_1c.py list` @@ -78,7 +78,7 @@ baseconf_aliases: |---------|----------| | `reindex` | обновить индекс | | `status` | размер БД, meta, список проиндексированных baseconf | -| `list` | выгрузки с `src/` в репозитории + что уже в индексе | +| `list` | источники индексации в репозитории + что уже в индексе | ### Режимы `reindex` @@ -95,7 +95,8 @@ baseconf_aliases: ### Что индексируется -- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений). +- **Метаданные конфигураций/расширений** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений). +- **Метаданные внешних обработок**: `/<ИмяОбработки>.xml` (как `ExternalDataProcessor`). - **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS). - **Связи движений** — таблица `register_records` (обратный индекс регистраторов). @@ -113,6 +114,9 @@ python index_1c.py reindex --full -j 12 # основная конфигурация + расширение, только документы и справочники python index_1c.py reindex -b main -b ext -t Documents,Catalogs -j 8 +# внешние обработки: развернуть папку в virtual baseconf +python index_1c.py reindex -b my-transfer.ext -j 8 + python index_1c.py status python index_1c.py list ``` diff --git a/VERSION b/VERSION index 1c09c74..42045ac 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.3 +0.3.4 diff --git a/index1c/build.py b/index1c/build.py index 4bf0b72..5a541e9 100644 --- a/index1c/build.py +++ b/index1c/build.py @@ -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) diff --git a/index1c/cli.py b/index1c/cli.py index eb77fa4..21cad2a 100644 --- a/index1c/cli.py +++ b/index1c/cli.py @@ -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С в репозитории (папка /src/). " + "Источник индексации 1С: /src/ или virtual .<Обработка>. " "Можно несколько раз или через запятую: -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С не найдена в репозитории (нет каталога /src/):", + "Источник индексации 1С не найден в репозитории " + "(нет /src/ или внешней обработки /.xml + //):", *(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( diff --git a/index1c/config.py b/index1c/config.py index be23c8b..3bd2de7 100644 --- a/index1c/config.py +++ b/index1c/config.py @@ -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С (/src/) в корне проекта.""" + """Все источники индексации в корне проекта. + + Возвращает: + - обычные конфигурации/расширения: (если есть /src/) + - внешние обработки: .<Обработка> для каталогов вида /*.xml + /<Обработка>/ + """ 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 " 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 для внешних обработок: + # . → ищем <...>/.xml + <...>// + 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 diff --git a/index1c/parse_meta.py b/index1c/parse_meta.py index 699be53..f52f746 100644 --- a/index1c/parse_meta.py +++ b/index1c/parse_meta.py @@ -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//*.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 diff --git a/index1c/parse_modules.py b/index1c/parse_modules.py index 22ea680..291c507 100644 --- a/index1c/parse_modules.py +++ b/index1c/parse_modules.py @@ -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: diff --git a/index_1c.py b/index_1c.py index 5c4d73d..d78f217 100755 --- a/index_1c.py +++ b/index_1c.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 -"""Переиндексация метаданных и модулей конфигураций 1С → cache/1c_meta/index.sqlite. +"""Переиндексация метаданных и модулей 1С (конфигурации + внешние обработки) → cache/1c_meta/index.sqlite. Параметры: --config, -c файл настроек утилиты (YAML) - --baseconf, -b какие выгрузки 1С индексировать (имя каталога с src/) + --baseconf, -b какие источники 1С индексировать (каталог с src/ или virtual external) Примеры: @@ -90,7 +90,8 @@ def cmd_reindex(args: argparse.Namespace) -> int: configs = _select_configs_for_reindex(args, root) if not configs: print( - "Нет конфигураций для индексации (нужна папка /src/).\n" + "Нет источников для индексации (нужна папка /src/ " + "или внешняя обработка /.xml + //).\n" "Укажите -b <имя-выгрузки> или проверьте: python index_1c.py list", file=sys.stderr, ) @@ -235,10 +236,10 @@ def build_parser() -> argparse.ArgumentParser: lc = sub.add_parser( "list", - help="Выгрузки в репозитории и в индексе", + help="Источники в репозитории и в индексе", parents=[tool_parent], formatter_class=argparse.RawDescriptionHelpFormatter, - description="Выгрузки 1С с src/ в репозитории и что уже в index.sqlite.", + description="Источники 1С в репозитории (src/ и external) и что уже в index.sqlite.", ) lc.set_defaults(func=cmd_list_configs)