3 Commits

Author SHA1 Message Date
mihailkudravcev 91521e6604 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.
2026-07-21 13:03:25 +03:00
mihailkudravcev 06e13395f8 если объект найден по реквизиту (колонка в FTS), в выдаче показывается только совпавший реквизит с типом значения — в том же формате дерева 2026-07-17 16:56:15 +03:00
mihailkudravcev 404659a545 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>
2026-07-17 11:09:51 +03:00
18 changed files with 719 additions and 220 deletions
+2
View File
@@ -8,3 +8,5 @@ __pycache__/
*.egg-info/ *.egg-info/
.pytest_cache/ .pytest_cache/
config.local.json config.local.json
config.local.yml
config.local.yaml
+44 -6
View File
@@ -1,10 +1,46 @@
# Changelog # Changelog
Все значимые изменения **tools/index** (индекс метаданных и модулей 1С) фиксируются в этом файле. Все значимые изменения утилиты **index** (индекс метаданных и модулей 1С) фиксируются в этом файле.
Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Формат: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Версионирование: [Semantic Versioning](https://semver.org/lang/ru/). Версионирование: [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` теперь учитывают не только `<name>/src/`, но и внешние обработки.
- Индексация metadata/modules для virtual external baseconf:
- metadata: `<root>/<ExternalDataProcessor>.xml`,
- модули: `*.bsl` только внутри `<root>/<ExternalDataProcessor>/`.
- Определение owner для модулей внешней обработки:
- `ExternalDataProcessor.<ИмяОбработки>` для `Ext/ObjectModule.bsl`,
- `ExternalDataProcessor.<ИмяОбработки>.Form.<ИмяФормы>` для модулей форм.
## [0.3.3] - 2026-07-17
### Changed
- **`query_1c.py search`**: если объект найден по реквизиту (колонка `fields_text` в FTS), в выдаче показывается только совпавший реквизит с типом значения — в том же формате дерева, что и `object --clear`.
## [0.3.2] - 2026-07-17
### Added
- `--config` в формате **YAML** (`.yml`/`.yaml`) с поддержкой комментариев; пример `config.example.yml`.
- Блок **`default_baseconfs`** в конфиге: `reindex` без `-b` индексирует только этот список; без блока — автосканирование `<name>/src/`.
- **Автопоиск** локального конфига без `--config`: `config.local.yml` / `config.local.yaml` / `config.local.json`.
### Changed
- Документация и тексты `--help`: обобщённые примеры вместо локальных имён выгрузок и проектов.
- Алиасы `--baseconf` — через `baseconf_aliases` в YAML-конфиге (не в коде).
- Список конфигураций для `reindex` без `-b`: автообнаружение или `default_baseconfs` из конфига.
## [0.3.1] - 2026-07-17 ## [0.3.1] - 2026-07-17
### Changed ### Changed
@@ -15,7 +51,7 @@
### Fixed ### Fixed
- `query_1c.py`: `-b` / `--baseconf` до подкоманды (`query_1c.py -b crm3-26 search …`) корректно применяется к запросу. - `query_1c.py`: `-b` / `--baseconf` до подкоманды (`query_1c.py -b my-config search …`) корректно применяется к запросу.
## [0.3.0] - 2026-07-16 ## [0.3.0] - 2026-07-16
@@ -48,7 +84,7 @@
### Changed ### Changed
- Параметр конфигурации 1С: **`--baseconf` / `-b`** (алиасы `target``crm3-26`, `source``crm3-dev`). - Параметр конфигурации 1С: **`--baseconf` / `-b`**; алиасы через `baseconf_aliases` в `--config`.
- **`--config` / `-c`** — только файл настроек утилиты (JSON), не выгрузка 1С. - **`--config` / `-c`** — только файл настроек утилиты (JSON), не выгрузка 1С.
- Информативные ошибки: нет индекса / нет папки `src/` / baseconf не проиндексирован / объект не найден (с подсказками). - Информативные ошибки: нет индекса / нет папки `src/` / baseconf не проиндексирован / объект не найден (с подсказками).
- Расширена документация `README.md` (примеры, режимы, применение в Cursor). - Расширена документация `README.md` (примеры, режимы, применение в Cursor).
@@ -67,6 +103,8 @@
- `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`. - `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`.
- Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`. - Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`.
[0.3.0]: https://git.p7net.ru/1c/index/-/tags/v0.3.0 [0.3.2]: v0.3.2
[0.2.0]: https://git.p7net.ru/1c/index/-/tags/v0.2.0 [0.3.1]: v0.3.1
[0.1.0]: https://git.p7net.ru/1c/index/-/tags/v0.1.0 [0.3.0]: v0.3.0
[0.2.0]: v0.2.0
[0.1.0]: v0.1.0
+78 -68
View File
@@ -1,6 +1,6 @@
# Индекс метаданных и модулей конфигураций 1С # Индекс метаданных и модулей конфигураций 1С
**Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.1**) · [CHANGELOG](CHANGELOG.md) **Версия:** см. [`VERSION`](VERSION) (текущая: **0.3.4**) · [CHANGELOG](CHANGELOG.md)
**Автор:** Michael BAG · [mk@p7net.ru](mailto:mk@p7net.ru) **Автор:** Michael BAG · [mk@p7net.ru](mailto:mk@p7net.ru)
**Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru) **Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru)
@@ -11,19 +11,16 @@
| [`index_1c.py`](index_1c.py) | построение и обновление индекса | | [`index_1c.py`](index_1c.py) | построение и обновление индекса |
| [`query_1c.py`](query_1c.py) | поиск и просмотр по готовому индексу | | [`query_1c.py`](query_1c.py) | поиск и просмотр по готовому индексу |
Репозиторий утилиты: <https://git.p7net.ru/1c/index.git>
Правило для Cursor (в корне CRM3-26): `.cursor/rules/1c-meta-index.mdc`
```bash ```bash
python tools/index/index_1c.py --version python index_1c.py --version
python tools/index/query_1c.py --version python query_1c.py --version
``` ```
--- ---
## Зачем это нужно ## Зачем это нужно
Обход десятков тысяч XML/BSL в `crm3-26/src` и соседних выгрузках через Grep в Cursor медленный. Индекс один раз разбирает метаданные и модули и даёт: Обход десятков тысяч XML/BSL в `<выгрузка>/src` через Grep в IDE медленный. Индекс один раз разбирает метаданные и модули и даёт:
| Задача | Команда | | Задача | Команда |
|--------|---------| |--------|---------|
@@ -32,9 +29,9 @@ python tools/index/query_1c.py --version
| Кто ссылается на справочник/документ | `query_1c.py refs` | | Кто ссылается на справочник/документ | `query_1c.py refs` |
| Документ ↔ регистры движений | `query_1c.py movements` | | Документ ↔ регистры движений | `query_1c.py movements` |
| Найти процедуру/фрагмент в BSL | `query_1c.py modules` | | Найти процедуру/фрагмент в BSL | `query_1c.py modules` |
| Сравнить целевую и исходную конфигурацию | `-b target` / `-b source` | | Сравнить несколько выгрузок в одном репозитории | `-b cfg1 -b cfg2` |
**Типичный workflow агента:** **Типичный workflow:**
`reindex` (после выгрузки) → `object` / `search``refs` / `modules` → открыть конкретный `.xml` / `.bsl`. `reindex` (после выгрузки) → `object` / `search``refs` / `modules` → открыть конкретный `.xml` / `.bsl`.
--- ---
@@ -43,22 +40,33 @@ python tools/index/query_1c.py --version
| Параметр | Короткий | Назначение | | Параметр | Короткий | Назначение |
|----------|----------|------------| |----------|----------|------------|
| `--config` | `-c` | Файл **настроек утилиты** (JSON). **Не** конфигурация 1С. См. [`config.example.json`](config.example.json). | | `--config` | `-c` | Файл **настроек утилиты** (YAML). **Не** конфигурация 1С. См. [`config.example.yml`](config.example.yml). |
| `--root` | `-r` | Корень проекта CRM3-26 (если запуск не из дерева проекта). | | `--root` | `-r` | Корень проекта с выгрузками 1С (если запуск не из дерева проекта). |
| `--baseconf` | `-b` | Выгрузка конфигурации 1С: папка `<name>/src/` в репозитории. Можно несколько раз. | | `--baseconf` | `-b` | Источник индексации: `<name>/src/` или virtual внешняя обработка `<dir>.<ИмяОбработки>`. Можно несколько раз. |
| `--version` | | Версия утилиты. | | `--version` | | Версия утилиты. |
Приоритет корня проекта: `--root` > `project_root` в `--config` > авто из расположения `tools/index/`. Приоритет корня проекта: `--root` > `project_root` в `--config` > авто (каталог с `index_1c.py` и `VERSION`).
Если `--config` не указан, утилита пытается автоматически найти
`config.local.yml`, `config.local.yaml` или `config.local.json`.
### Алиасы `--baseconf` ### Алиасы `--baseconf`
| Алиас | Папка | Краткие имена задаются в файле `--config`, поле **`baseconf_aliases`**:
|-------|-------|
| `target`, `crm3_26` | `crm3-26` |
| `source`, `crm3_old`, `crm3-old`, `crm3_dev` | `crm3-dev` |
Другие выгрузки без алиаса: `crm3-26.rhana`, `crm3-dev.rhana`, `ws-rhana`, `bu-corp`, … ```yaml
Полный список: `python tools/index/index_1c.py list` project_root: "/path/to/project"
baseconf_aliases:
main: "my-config"
ext: "my-config.ext"
```
После этого `-b main` эквивалентно `-b my-config`.
Если в YAML задан блок `default_baseconfs`, то `reindex` без `-b` использует этот список.
Если блока нет — `reindex` без `-b` индексирует **все** каталоги с `src/` и найденные внешние обработки.
`query_1c.py` без `-b` ищет по **всем** конфигурациям в индексе.
Список выгрузок: `python index_1c.py list`
--- ---
@@ -70,7 +78,7 @@ python tools/index/query_1c.py --version
|---------|----------| |---------|----------|
| `reindex` | обновить индекс | | `reindex` | обновить индекс |
| `status` | размер БД, meta, список проиндексированных baseconf | | `status` | размер БД, meta, список проиндексированных baseconf |
| `list` | выгрузки с `src/` в репозитории + что уже в индексе | | `list` | источники индексации в репозитории + что уже в индексе |
### Режимы `reindex` ### Режимы `reindex`
@@ -78,16 +86,17 @@ python tools/index/query_1c.py --version
|-------|-----------| |-------|-----------|
| `reindex -j 12` | инкремент: только файлы с изменившимися `mtime`/`size` | | `reindex -j 12` | инкремент: только файлы с изменившимися `mtime`/`size` |
| `reindex --full -j 12` | удалить всю БД и собрать заново все известные выгрузки | | `reindex --full -j 12` | удалить всю БД и собрать заново все известные выгрузки |
| `reindex --full -b crm3-26` | пересобрать только указанную конфигурацию | | `reindex --full -b my-config` | пересобрать только указанную конфигурацию |
| `reindex -b target -t Documents,Catalogs` | только выбранные типы объектов | | `reindex -b main -t Documents,Catalogs` | только выбранные типы объектов |
| `reindex -b ws-rhana --no-modules` | метаданные без BSL | | `reindex -b my-config --no-modules` | метаданные без BSL |
| `reindex --md` | дополнительно Markdown в `cache/1c_meta/md/<baseconf>/` | | `reindex --md` | дополнительно Markdown в `cache/1c_meta/md/<baseconf>/` |
`-j` / `--workers` — число потоков (по умолчанию 4–16). `-j` / `--workers` — число потоков (по умолчанию 4–16).
### Что индексируется ### Что индексируется
- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений). - **Метаданные конфигураций/расширений** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений).
- **Метаданные внешних обработок**: `<dir>/<ИмяОбработки>.xml` (как `ExternalDataProcessor`).
- **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS). - **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS).
- **Связи движений** — таблица `register_records` (обратный индекс регистраторов). - **Связи движений** — таблица `register_records` (обратный индекс регистраторов).
@@ -96,17 +105,20 @@ python tools/index/query_1c.py --version
### Примеры переиндексации ### Примеры переиндексации
```bash ```bash
# после обновления выгрузки целевой конфигурации # после обновления одной выгрузки
python tools/index/index_1c.py reindex -b target -j 12 python index_1c.py reindex -b my-config -j 12
# полная пересборка всего проекта # полная пересборка всего проекта
python tools/index/index_1c.py reindex --full -j 12 python index_1c.py reindex --full -j 12
# только документы и справочники исходной + расширения # основная конфигурация + расширение, только документы и справочники
python tools/index/index_1c.py reindex -b source -b crm3-dev.rhana -t Documents,Catalogs -j 8 python index_1c.py reindex -b main -b ext -t Documents,Catalogs -j 8
python tools/index/index_1c.py status # внешние обработки: развернуть папку в virtual baseconf
python tools/index/index_1c.py list python index_1c.py reindex -b my-transfer.ext -j 8
python index_1c.py status
python index_1c.py list
``` ```
--- ---
@@ -119,10 +131,10 @@ python tools/index/index_1c.py list
| Команда | Аргумент | Результат | Полезные опции | | Команда | Аргумент | Результат | Полезные опции |
|---------|----------|-----------|----------------| |---------|----------|-----------|----------------|
| `search` | строка | объекты метаданных (FTS) | `-b`, `--type`, `--limit` | | `search` | строка | объекты метаданных (FTS); при совпадении по реквизиту — дерево найденного поля | `-b`, `--type`, `--limit` |
| `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` | | `object` | имя/синоним объекта **или** реквизита | дерево структуры | `-b`, `--type`, `--clear`, `--limit` |
| `show` | имя объекта | полная карточка JSON | `-b` | | `show` | имя объекта | полная карточка JSON | `-b` |
| `refs` | `Catalog.Партнеры` / `ЗаказКлиента` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) | | `refs` | `Catalog.Customers` / `Contract` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
| `movements` | документ или регистр | регистратор ↔ регистры движений | `-b`, `--type`, `--limit` | | `movements` | документ или регистр | регистратор ↔ регистры движений | `-b`, `--type`, `--limit` |
| `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` | | `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` |
| `module` | путь или owner | символы модуля, строки | `-b` | | `module` | путь или owner | символы модуля, строки | `-b` |
@@ -131,22 +143,22 @@ python tools/index/index_1c.py list
```bash ```bash
# полная структура документа # полная структура документа
python tools/index/query_1c.py object ЗаказКлиента -b target python query_1c.py object Contract -b my-config
# только реквизиты с именем/синонимом «Партнер» в документах # только реквизиты с именем/синонимом «Partner» в документах
python tools/index/query_1c.py object Партнер -b target --type Document --clear python query_1c.py object Partner -b my-config --type Document --clear
# совпал сам объект → --clear печатает только заголовок # совпал сам объект → --clear печатает только заголовок
python tools/index/query_1c.py object ЗаказКлиента -b target --clear python query_1c.py object Contract -b my-config --clear
``` ```
Формат строки реквизита: Формат строки реквизита:
```text ```text
├── `Attribute.Партнер` — Клиент [обяз.] : CatalogRef.Партнеры ├── `Attribute.Partner` — Клиент [обяз.] : CatalogRef.Customers
├── `Attribute.СуммаДокумента` — … [необяз.] : DefinedType.ДенежнаяСуммаЛюбогоЗнака ├── `Attribute.Amount` — … [необяз.] : DefinedType.MoneyAmount
├── `TabularSection.Товары` — Товары ├── `TabularSection.Lines` — Строки
│ └── `Column.Номенклатура` — … [обяз.] : CatalogRef.Номенклатура │ └── `Column.Item` — … [обяз.] : CatalogRef.Items
``` ```
- **[обяз.]** — `FillChecking=ShowError`; **[необяз.]** — `DontCheck` - **[обяз.]** — `FillChecking=ShowError`; **[необяз.]** — `DontCheck`
@@ -156,24 +168,24 @@ python tools/index/query_1c.py object ЗаказКлиента -b target --clear
### `refs` — кто ссылается ### `refs` — кто ссылается
```bash ```bash
python tools/index/query_1c.py refs "ЗаказКлиента" python query_1c.py refs "Contract"
python tools/index/query_1c.py refs Catalog.Партнеры -b target --limit 50 python query_1c.py refs Catalog.Customers -b my-config --limit 50
python tools/index/query_1c.py refs Document.ЗаказКлиента -b source python query_1c.py refs Document.Contract -b main
``` ```
Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**. Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**.
Составной тип поля: `→ DocumentRef.ЗаказКлиента (среди прочего: DocumentRef.…)`. Составной тип поля: `→ DocumentRef.Contract (среди прочего: DocumentRef.…)`.
### `movements` — регистратор ↔ регистры движений ### `movements` — регистратор ↔ регистры движений
```bash ```bash
# документ → регистры, в которых он регистратор # документ → регистры, в которых он регистратор
python tools/index/query_1c.py movements ЗаказКлиента -b target python query_1c.py movements Contract -b my-config
python tools/index/query_1c.py movements Document.РеализацияТоваровУслуг -b target python query_1c.py movements Document.Sales -b my-config
# регистр → документы-регистраторы # регистр → документы-регистраторы
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target python query_1c.py movements AccumulationRegister.Stock -b my-config
python tools/index/query_1c.py movements ТоварыНаСкладах -b target --type AccumulationRegister python query_1c.py movements Stock -b my-config --type AccumulationRegister
``` ```
Данные из свойства метаданных `RegisterRecords` (таблица `register_records` в индексе). Данные из свойства метаданных `RegisterRecords` (таблица `register_records` в индексе).
@@ -182,18 +194,18 @@ python tools/index/query_1c.py movements ТоварыНаСкладах -b targe
### `modules` / `module` ### `modules` / `module`
```bash ```bash
python tools/index/query_1c.py modules "ОбработатьHealth" -b ws-rhana python query_1c.py modules "ProcessData" -b my-config
python tools/index/query_1c.py modules "РВС_Transfer" -b ws-rhana -n python query_1c.py modules "MyModule" -b my-config.ext -n
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы -b ws-rhana python query_1c.py module CommonModules/MyModule -b my-config
``` ```
### Прочие примеры ### Прочие примеры
```bash ```bash
python tools/index/query_1c.py search "ЗаказКлиента" python query_1c.py search "Contract"
python tools/index/query_1c.py search "Партнер" --type Document -b crm3-26 python query_1c.py search "Partner" --type Document -b my-config
python tools/index/query_1c.py show Document.ЗаказКлиента -b target python query_1c.py show Document.Contract -b main
python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rhana python query_1c.py search "Partner" -b main -b ext
``` ```
### Ошибки и подсказки ### Ошибки и подсказки
@@ -201,7 +213,7 @@ python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rha
| Ситуация | Поведение | | Ситуация | Поведение |
|----------|-----------| |----------|-----------|
| Нет `index.sqlite` | подсказка `index_1c.py reindex --full` | | Нет `index.sqlite` | подсказка `index_1c.py reindex --full` |
| `-b` без папки `src/` | список доступных выгрузок + алиасы | | `-b` без папки `src/` | список доступных выгрузок |
| `-b` есть в репо, нет в индексе | список проиндексированных + команда `reindex -b …` | | `-b` есть в репо, нет в индексе | список проиндексированных + команда `reindex -b …` |
| `show` / `object` не нашли | похожие имена из индекса | | `show` / `object` не нашли | похожие имена из индекса |
@@ -210,29 +222,29 @@ python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rha
## Файл настроек утилиты (`--config`) ## Файл настроек утилиты (`--config`)
```bash ```bash
cp tools/index/config.example.json tools/index/config.local.json cp config.example.yml config.local.yml
# project_root: "/path/to/crm3-26" # project_root: "/path/to/project"
python tools/index/query_1c.py --config tools/index/config.local.json search "Заказ" python query_1c.py --config config.local.yml search "Contract"
``` ```
`config.local.json` в `.gitignore` (локальные пути). `config.local.yml` в `.gitignore` (локальные пути и алиасы).
Legacy-вариант `config.local.json` тоже поддерживается и подхватывается авто-поиском.
--- ---
## Применение в Cursor ## Применение в Cursor / IDE
1. После выгрузки из EDT/конфигуратора: `python tools/index/index_1c.py reindex -j 12` (или `-b target`). 1. После выгрузки из EDT/конфигуратора: `python index_1c.py reindex -j 12` (или `-b my-config`).
2. Метаданные и связи — через `query_1c.py`, не Grep по всему `src/`. 2. Метаданные и связи — через `query_1c.py`, не Grep по всему `src/`.
3. Разделять контекст: `-b target` vs `-b source`. 3. При нескольких выгрузках — ограничивать `-b` нужной baseconf.
4. Код: `modules` → открыть `rel_path`; структура документа: `object`. 4. Код: `modules` → открыть `rel_path`; структура документа: `object`.
5. Правило агента: `.cursor/rules/1c-meta-index.mdc`.
--- ---
## Артефакты кэша ## Артефакты кэша
```text ```text
cache/1c_meta/ # в корне проекта CRM3-26 (gitignore) cache/1c_meta/ # в корне проекта (обычно в .gitignore)
index.sqlite # единая БД index.sqlite # единая БД
INDEX.md INDEX.md
manifest.json manifest.json
@@ -251,8 +263,6 @@ Per-config `search.sqlite` устарели и не используются.
| **E-mail** | [mk@p7net.ru](mailto:mk@p7net.ru) | | **E-mail** | [mk@p7net.ru](mailto:mk@p7net.ru) |
| **Предложения, вопросы, ошибки** | пишите на **mk@p7net.ru** | | **Предложения, вопросы, ошибки** | пишите на **mk@p7net.ru** |
Репозиторий: <https://git.p7net.ru/1c/index.git>
--- ---
## Лицензирование ## Лицензирование
+1 -1
View File
@@ -1 +1 @@
0.3.1 0.3.4
-4
View File
@@ -1,4 +0,0 @@
{
"project_root": "/path/to/crm3-26",
"comment": "Файл настроек утилиты tools/index (НЕ конфигурация 1С). Копируйте в config.local.json и укажите --config config.local.json"
}
+24
View File
@@ -0,0 +1,24 @@
# Пример локальной конфигурации tools/index.
# Скопируйте в config.local.yml и передавайте:
# python index_1c.py --config config.local.yml reindex
# python query_1c.py --config config.local.yml search "..."
#
# ВАЖНО:
# - Это файл настроек утилиты, НЕ конфигурация 1С.
# - Файл обычно не коммитят (локальные пути/алиасы).
# Корень проекта, в котором лежат выгрузки (<name>/src/).
project_root: "/path/to/project"
# Короткие алиасы для --baseconf.
# Пример: -b main эквивалентно -b my-config
baseconf_aliases:
main: "my-config"
ext: "my-config.ext"
# Необязательный whitelist baseconf для reindex без -b.
# Если блок задан, то reindex (без -b) возьмет ТОЛЬКО эти конфигурации.
# Если блока нет — работает автосканирование всех <name>/src/ в project_root.
default_baseconfs:
- main
- ext
+1 -1
View File
@@ -2,7 +2,7 @@
from pathlib import Path from pathlib import Path
__version__ = "0.3.1" __version__ = "0.3.2"
__status__ = "in development" __status__ = "in development"
__author__ = "Michael BAG" __author__ = "Michael BAG"
__email__ = "mk@p7net.ru" __email__ = "mk@p7net.ru"
+13 -2
View File
@@ -172,7 +172,13 @@ def _index_one_config(
existing = dbmod.get_file_index(conn, cfg.name) if not full else {} existing = dbmod.get_file_index(conn, cfg.name) if not full else {}
seen_paths: set[str] = set() 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_jobs: list[tuple[str, str, str]] = []
meta_skipped = 0 meta_skipped = 0
for otype, path in meta_files: for otype, path in meta_files:
@@ -222,7 +228,12 @@ def _index_one_config(
mod_skipped = 0 mod_skipped = 0
if index_modules: if index_modules:
mod_jobs: list[tuple[str, str, str]] = [] 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() rel = path.relative_to(cfg.src).as_posix()
seen_paths.add(rel) seen_paths.add(rel)
mtime_ns, size = _file_stamp(path) mtime_ns, size = _file_stamp(path)
+94 -21
View File
@@ -3,7 +3,6 @@
from __future__ import annotations from __future__ import annotations
import argparse import argparse
import json
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -21,25 +20,89 @@ class BaseconfError(Exception):
def load_tool_config(path: Path) -> dict[str, Any]: def load_tool_config(path: Path) -> dict[str, Any]:
"""Файл настроек утилиты index (JSON). Не путать с конфигурацией 1С.""" """Файл настроек утилиты index (YAML; legacy JSON поддерживается)."""
ext = path.suffix.lower()
try: try:
data = json.loads(path.read_text(encoding="utf-8")) raw = path.read_text(encoding="utf-8")
except OSError as e: except OSError as e:
raise BaseconfError(f"Не удалось прочитать --config {path}: {e}", exit_code=1) from e raise BaseconfError(f"Не удалось прочитать --config {path}: {e}", exit_code=1) from e
except json.JSONDecodeError as 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 raise BaseconfError(f"Некорректный JSON в --config {path}: {e}", exit_code=1) from e
if not isinstance(data, dict): if not isinstance(data, dict):
raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1) raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1)
return data 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}: ожидается 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: def resolve_project_root(args: argparse.Namespace) -> Path:
"""Корень проекта: --root → project_root из --config → авто.""" """Корень проекта: --root → --config/auto-config → авто."""
if getattr(args, "root", None): if getattr(args, "root", None):
return Path(args.root).resolve() return Path(args.root).resolve()
cfg_path = getattr(args, "config", None) 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: if cfg_path:
data = load_tool_config(Path(cfg_path)) data = load_tool_config(Path(cfg_path))
apply_tool_config(data)
pr = data.get("project_root") pr = data.get("project_root")
if pr: if pr:
return Path(pr).resolve() return Path(pr).resolve()
@@ -52,13 +115,16 @@ def add_tool_args(parser: argparse.ArgumentParser) -> None:
"--config", "--config",
"-c", "-c",
metavar="FILE", metavar="FILE",
help="JSON с настройками утилиты (project_root и др.), см. config.example.json", help=(
"YAML с настройками утилиты (project_root и др.), см. config.example.yml. "
"Без параметра: автопоиск config.local.yml/.yaml/.json"
),
) )
parser.add_argument( parser.add_argument(
"--root", "--root",
"-r", "-r",
metavar="DIR", metavar="DIR",
help="Корень проекта CRM3-26 (перекрывает project_root из --config)", help="Корень проекта с выгрузками 1С (перекрывает project_root из --config)",
) )
@@ -69,7 +135,10 @@ def add_baseconf_args(
) -> None: ) -> None:
"""Фильтр по выгрузке конфигурации 1С в репозитории.""" """Фильтр по выгрузке конфигурации 1С в репозитории."""
if mode == "index": if mode == "index":
default_hint = "Без параметра — индексировать все выгрузки с src/ в репозитории." default_hint = (
"Без параметра — default_baseconfs из --config; "
"если блок не задан, индексировать все выгрузки с src/ и внешние обработки."
)
else: else:
default_hint = "Без параметра — поиск по всем проиндексированным." default_hint = "Без параметра — поиск по всем проиндексированным."
parser.add_argument( parser.add_argument(
@@ -79,9 +148,9 @@ def add_baseconf_args(
metavar="NAME", metavar="NAME",
dest="baseconf", dest="baseconf",
help=( help=(
"Конфигурация 1С в репозитории (папка <name>/src/). " "Источник индексации 1С: <name>/src/ или virtual <dir>.<Обработка>. "
"Можно несколько раз или через запятую: -b target,source. " "Можно несколько раз или через запятую: -b cfg1,cfg2. "
"Алиасы: target→crm3-26, source→crm3-dev. " "Краткие имена (алиасы) — в baseconf_aliases файла --config. "
f"{default_hint}" f"{default_hint}"
), ),
) )
@@ -165,34 +234,38 @@ def validate_baseconfs(
require_indexed: bool = True, require_indexed: bool = True,
) -> list[str]: ) -> list[str]:
""" """
Проверить --baseconf: папка src/, наличие в индексе. Проверить --baseconf: src-выгрузка или virtual внешняя обработка, наличие в индексе.
Возвращает канонические имена. Иначе BaseconfError с подсказками. Возвращает канонические имена. Иначе BaseconfError с подсказками.
""" """
if not names: if not names:
return [] return []
resolved = resolve_baseconf_names(names) resolved = resolve_baseconf_names(names)
available = {c.name for c in discover_configs(project_root, None)}
missing_src: list[str] = [] missing_src: list[str] = []
for name in resolved: 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) missing_src.append(name)
if missing_src: if missing_src:
available = [c.name for c in discover_configs(project_root, None)] available_list = sorted(available)
lines = [ lines = [
"Конфигурация 1С не найдена в репозитории (нет каталога <name>/src/):", "Источник индексации 1С не найден в репозитории "
"(нет <name>/src/ или внешней обработки <dir>/<name>.xml + <dir>/<name>/):",
*(f" - {n}" for n in missing_src), *(f" - {n}" for n in missing_src),
"", "",
"Доступные выгрузки в проекте:", "Доступные выгрузки в проекте:",
] ]
if available: if available_list:
lines.extend(f" - {n}" for n in available) lines.extend(f" - {n}" for n in available_list)
else: else:
lines.append(" (нет)") lines.append(" (нет)")
lines.extend( lines.extend(
[ [
"", "",
"Алиасы: target→crm3-26, source→crm3-dev, crm3_old→crm3-dev", "Краткие имена (алиасы) для -b задаются в baseconf_aliases файла --config.",
] ]
) )
raise BaseconfError("\n".join(lines)) raise BaseconfError("\n".join(lines))
@@ -216,7 +289,7 @@ def validate_baseconfs(
lines.extend( 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)) raise BaseconfError("\n".join(lines))
@@ -297,7 +370,7 @@ def index_db_missing_message(project_root: Path) -> str:
return ( return (
f"Индекс не найден: {path}\n" f"Индекс не найден: {path}\n"
"Сначала выполните:\n" "Сначала выполните:\n"
" python tools/index/index_1c.py reindex --full -j 12\n" " python index_1c.py reindex --full -j 12\n"
"или для одной конфигурации:\n" "или для одной конфигурации:\n"
" python tools/index/index_1c.py reindex --full -b crm3-26" " python index_1c.py reindex --full -b <имя-выгрузки>"
) )
+162 -27
View File
@@ -2,31 +2,14 @@
from __future__ import annotations from __future__ import annotations
import re
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
# Папки выгрузок относительно корня проекта (есть src/). # Алиасы имён конфигураций 1С для CLI (--baseconf / -b), задаются через --config
DEFAULT_CONFIGS: tuple[str, ...] = ( # (поле baseconf_aliases в YAML). См. config.example.yml.
"crm3-26", _RUNTIME_ALIASES: dict[str, str] = {}
"crm3-26.rhana", _RUNTIME_DEFAULT_BASECONFS: list[str] = []
"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",
}
# Папка EDT → канонический тип (английский идентификатор выгрузки). # Папка EDT → канонический тип (английский идентификатор выгрузки).
OBJECT_TYPE_FOLDERS: dict[str, str] = { OBJECT_TYPE_FOLDERS: dict[str, str] = {
@@ -71,6 +54,7 @@ OBJECT_TYPE_FOLDERS: dict[str, str] = {
"XDTOPackages": "XDTOPackage", "XDTOPackages": "XDTOPackage",
"Bots": "Bot", "Bots": "Bot",
"ExternalDataSources": "ExternalDataSource", "ExternalDataSources": "ExternalDataSource",
"ExternalDataProcessors": "ExternalDataProcessor",
"Languages": "Language", "Languages": "Language",
} }
@@ -110,6 +94,10 @@ TYPE_ALIASES: dict[str, str] = {
"dataprocessors": "DataProcessor", "dataprocessors": "DataProcessor",
"обработка": "DataProcessor", "обработка": "DataProcessor",
"обработки": "DataProcessor", "обработки": "DataProcessor",
"externaldataprocessor": "ExternalDataProcessor",
"externaldataprocessors": "ExternalDataProcessor",
"внешняяобработка": "ExternalDataProcessor",
"внешниеобработки": "ExternalDataProcessor",
"report": "Report", "report": "Report",
"reports": "Report", "reports": "Report",
"отчет": "Report", "отчет": "Report",
@@ -189,16 +177,105 @@ class ConfigPaths:
name: str name: str
root: Path root: Path
src: Path src: Path
source_kind: str = "src" # src | external_processor
external_name: str = ""
def project_root_from_here() -> Path: def project_root_from_here() -> Path:
"""tools/index/index1c → корень проекта.""" """Корень проекта: каталог с index_1c.py и VERSION."""
return Path(__file__).resolve().parents[3] 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: def resolve_config_name(name: str) -> str:
key = name.strip() 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]:
"""Все источники индексации в корне проекта.
Возвращает:
- обычные конфигурации/расширения: <name> (если есть <name>/src/)
- внешние обработки: <dir>.<Обработка> для каталогов вида <dir>/*.xml + <dir>/<Обработка>/
"""
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)
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: def resolve_object_type(name: str) -> str:
@@ -217,13 +294,71 @@ def resolve_object_type(name: str) -> str:
def discover_configs(project_root: Path, names: list[str] | None = None) -> list[ConfigPaths]: 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] = [] 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: for name in wanted:
root = project_root / name root = project_root / name
src = root / "src" src = root / "src"
if src.is_dir(): 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 return found
+1 -1
View File
@@ -245,7 +245,7 @@ def init_db(conn: sqlite3.Connection) -> None:
if ver > SCHEMA_VERSION: if ver > SCHEMA_VERSION:
raise RuntimeError( raise RuntimeError(
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). " f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
"Обновите tools/index." "Обновите утилиту index до более новой версии."
) )
if ver < SCHEMA_VERSION: if ver < SCHEMA_VERSION:
migrate_schema(conn, ver) migrate_schema(conn, ver)
+202 -21
View File
@@ -3,12 +3,15 @@
from __future__ import annotations from __future__ import annotations
import json import json
import re
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
_QUERY_TOKEN_RE = re.compile(r"[\wА-Яа-яЁё]+", flags=re.UNICODE)
FILL_LABEL = { FILL_LABEL = {
"ShowError": "обяз.", "ShowError": "обяз.",
@@ -43,6 +46,174 @@ def _field_matches(f: dict[str, Any], q: str, qn: str) -> bool:
return name_cf == qn or syn_cf == qn or qn in syn_parts return name_cf == qn or syn_cf == qn or qn in syn_parts
def _query_tokens(query: str) -> list[str]:
return _QUERY_TOKEN_RE.findall(query or "")
def _words_cf(text: str) -> list[str]:
return [w.casefold() for w in _QUERY_TOKEN_RE.findall(text or "")]
def _tokens_match_text(tokens: list[str], text: str) -> bool:
"""Как FTS search: каждый токен — префикс какого-либо слова в тексте."""
if not tokens:
return False
words = _words_cf(text)
if not words:
return False
for tok in tokens:
t = tok.casefold()
if not any(w.startswith(t) for w in words):
return False
return True
def object_header_matches_search(row: dict[str, Any], query: str) -> bool:
"""Совпадение по имени/синониму/комментарию объекта (не по реквизитам)."""
tokens = _query_tokens(query)
if not tokens:
return False
for part in (
row.get("name") or "",
row.get("full_name") or "",
row.get("synonym") or "",
row.get("comment") or "",
):
if _tokens_match_text(tokens, part):
return True
return False
def field_matches_search_query(f: dict[str, Any], query: str) -> bool:
"""Совпадение реквизита с поисковым запросом (префикс по словам, как FTS)."""
tokens = _query_tokens(query)
if not tokens:
return False
text = " ".join(
filter(
None,
[
f.get("kind") or "",
f.get("name") or "",
f.get("synonym") or "",
f.get("comment") or "",
f.get("tabular_section") or "",
" ".join(f.get("types") or []),
],
)
)
return _tokens_match_text(tokens, text)
def _field_dict_from_db_row(row: sqlite3.Row) -> dict[str, Any]:
return {
"kind": row["kind"],
"name": row["name"],
"synonym": row["synonym"] or "",
"comment": row["comment"] or "" if "comment" in row.keys() else "",
"types": [t.strip() for t in (row["types"] or "").split(",") if t.strip()],
"tabular_section": row["tabular_section"] or "",
"refs": json.loads(row["refs_json"] or "[]"),
"fill_checking": "",
}
def find_fields_for_search_query(
conn: sqlite3.Connection,
object_id: int,
query: str,
) -> list[dict[str, Any]]:
"""Реквизиты объекта, совпавшие с FTS-запросом search."""
rows = conn.execute(
"""
SELECT kind, name, synonym, comment, types, tabular_section, refs_json
FROM fields
WHERE object_id = ?
ORDER BY tabular_section, kind, name
""",
(object_id,),
)
matched: list[dict[str, Any]] = []
seen: set[tuple[str, str, str]] = set()
for row in rows:
fdict = _field_dict_from_db_row(row)
if not field_matches_search_query(fdict, query):
continue
sig = (fdict["tabular_section"], fdict["name"], fdict["kind"])
if sig in seen:
continue
seen.add(sig)
matched.append(fdict)
return matched
def enrich_matched_fields(obj: dict[str, Any], matched: list[dict[str, Any]]) -> None:
"""Подтянуть FillChecking и типы из актуальной структуры объекта."""
if not matched or not obj.get("fields"):
return
by_f = {
(f.get("tabular_section") or "", f.get("name") or "", f.get("kind") or ""): f
for f in obj["fields"]
}
for mf in matched:
key = (mf.get("tabular_section") or "", mf.get("name") or "", mf.get("kind") or "")
src = by_f.get(key)
if not src:
continue
mf["fill_checking"] = src.get("fill_checking") or ""
mf["types"] = src.get("types") or mf.get("types")
mf["synonym"] = src.get("synonym") or mf.get("synonym")
mf["refs"] = src.get("refs") or mf.get("refs")
def load_object_structure_for_row(
conn: sqlite3.Connection,
row: dict[str, Any],
) -> dict[str, Any]:
match = {
"config": row["config"],
"full_name": row["full_name"],
"path": row.get("path"),
"object_type": row.get("object_type"),
}
if row.get("json"):
match["json"] = row["json"]
elif row.get("object_id"):
jrow = conn.execute(
"SELECT json FROM objects WHERE id=?", (row["object_id"],)
).fetchone()
match["json"] = jrow["json"] if jrow else "{}"
else:
jrow = conn.execute(
"SELECT json FROM objects WHERE config=? AND full_name=?",
(row["config"], row["full_name"]),
).fetchone()
match["json"] = jrow["json"] if jrow else "{}"
return load_object_structure(match)
def annotate_search_hits(
conn: sqlite3.Connection,
rows: list[dict[str, Any]],
query: str,
) -> list[dict[str, Any]]:
"""Уточнить search: object — по шапке; field — по реквизитам в fields_text."""
for row in rows:
if object_header_matches_search(row, query):
row["match_kind"] = "object"
row["matched_fields"] = []
continue
oid = row.get("object_id")
matched = find_fields_for_search_query(conn, int(oid), query) if oid else []
if matched:
row["match_kind"] = "field"
row["matched_fields"] = matched
else:
row["match_kind"] = "object"
row["matched_fields"] = []
return rows
def find_structure_matches( def find_structure_matches(
conn: sqlite3.Connection, conn: sqlite3.Connection,
query: str, query: str,
@@ -239,27 +410,10 @@ def format_field_line(
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}" return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
def format_object_tree( def _select_matched_field_nodes(
obj: dict[str, Any], fields: list[dict[str, Any]],
*, matched_fields: list[dict[str, Any]],
clear: bool = False, ) -> tuple[list[dict[str, Any]], set[tuple[str, str, str]], set[str]]:
matched_fields: list[dict[str, Any]] | None = None,
match_kind: str = "object",
) -> str:
"""Дерево реквизитов объекта. clear — только совпадения."""
lines: list[str] = []
syn = obj.get("synonym") or ""
syn_s = f"{syn}" if syn else ""
cfg = obj.get("config") or obj.get("_config") or ""
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
if cfg:
header = f"[{cfg}] {header}"
lines.append(header)
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
if clear:
if match_kind == "field" and matched_fields:
want: set[tuple[str, str, str]] = set() want: set[tuple[str, str, str]] = set()
need_ts: set[str] = set() need_ts: set[str] = set()
for mf in matched_fields: for mf in matched_fields:
@@ -283,6 +437,33 @@ def format_object_tree(
selected.append(f) selected.append(f)
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts: elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
selected.append(f) selected.append(f)
return selected, want, need_ts
def format_object_tree(
obj: dict[str, Any],
*,
clear: bool = False,
matched_fields: list[dict[str, Any]] | None = None,
match_kind: str = "object",
omit_header: bool = False,
) -> str:
"""Дерево реквизитов объекта. clear — только совпадения."""
lines: list[str] = []
syn = obj.get("synonym") or ""
syn_s = f"{syn}" if syn else ""
cfg = obj.get("config") or obj.get("_config") or ""
header = f"`{obj.get('full_name') or obj.get('object_type') + '.' + obj.get('name')}`{syn_s}"
if cfg:
header = f"[{cfg}] {header}"
if not omit_header:
lines.append(header)
fields: list[dict[str, Any]] = list(obj.get("fields") or [])
if clear:
if match_kind == "field" and matched_fields:
selected, want, need_ts = _select_matched_field_nodes(fields, matched_fields)
if not selected: if not selected:
lines.append(" (совпавшие реквизиты не найдены в структуре)") lines.append(" (совпавшие реквизиты не найдены в структуре)")
return "\n".join(lines) return "\n".join(lines)
+16 -1
View File
@@ -336,12 +336,27 @@ def iter_object_xml_files(
src: Path, src: Path,
types: set[str] | None = None, types: set[str] | None = None,
skip_types: set[str] | None = None, skip_types: set[str] | None = None,
*,
source_kind: str = "src",
external_name: str = "",
) -> list[tuple[str, Path]]: ) -> list[tuple[str, Path]]:
"""Список (object_type, xml_path) только верхний уровень src/<Folder>/*.xml.""" """Список (object_type, xml_path) для обычной выгрузки или external processing."""
skip = skip_types or set() skip = skip_types or set()
out: list[tuple[str, Path]] = [] out: list[tuple[str, Path]] = []
if not src.is_dir(): if not src.is_dir():
return out 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()): for folder in sorted(src.iterdir()):
if not folder.is_dir(): if not folder.is_dir():
continue continue
+6
View File
@@ -53,6 +53,12 @@ def _owner_from_rel(rel: Path) -> str:
parts = rel.parts parts = rel.parts
if not parts: if not parts:
return "" 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] folder = parts[0]
otype = OBJECT_TYPE_FOLDERS.get(folder) otype = OBJECT_TYPE_FOLDERS.get(folder)
if otype and len(parts) >= 2: if otype and len(parts) >= 2:
+9 -5
View File
@@ -10,6 +10,7 @@ from typing import Any
from . import db as dbmod from . import db as dbmod
from .config import cache_dir from .config import cache_dir
from .object_view import annotate_search_hits
def _fts_query(raw: str) -> str: def _fts_query(raw: str) -> str:
@@ -23,7 +24,7 @@ def open_index(project_root: Path, *, readonly: bool = True) -> sqlite3.Connecti
path = dbmod.global_db_path(cache_dir(project_root)) path = dbmod.global_db_path(cache_dir(project_root))
if not path.is_file(): if not path.is_file():
raise FileNotFoundError( raise FileNotFoundError(
f"Индекс не найден: {path}. Запустите: python tools/index/index_1c.py reindex --full" f"Индекс не найден: {path}. Запустите: python index_1c.py reindex --full"
) )
# мягкая миграция схемы (например 2→3: таблица register_records) # мягкая миграция схемы (например 2→3: таблица register_records)
probe = dbmod.connect(path, readonly=True) probe = dbmod.connect(path, readonly=True)
@@ -266,7 +267,8 @@ def search_objects(
limit: int = 30, limit: int = 30,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
exact_sql = """ exact_sql = """
SELECT config, full_name, object_type, name, synonym, comment, path, '' AS snip, -100.0 AS score SELECT config, full_name, object_type, name, synonym, comment, path,
id AS object_id, '' AS snip, -100.0 AS score
FROM objects FROM objects
WHERE (name = ? OR full_name = ? OR full_name LIKE ?) WHERE (name = ? OR full_name = ? OR full_name LIKE ?)
""" """
@@ -286,6 +288,7 @@ def search_objects(
fetch_n = max(limit * 5, 50) fetch_n = max(limit * 5, 50)
sql = """ sql = """
SELECT o.config, o.full_name, o.object_type, o.name, o.synonym, o.comment, o.path, SELECT o.config, o.full_name, o.object_type, o.name, o.synonym, o.comment, o.path,
o.id AS object_id,
snippet(objects_fts, 0, '[', ']', '', 12) AS snip, snippet(objects_fts, 0, '[', ']', '', 12) AS snip,
bm25(objects_fts, 10.0, 5.0, 2.0, 1.0) AS score bm25(objects_fts, 10.0, 5.0, 2.0, 1.0) AS score
FROM objects_fts FROM objects_fts
@@ -307,7 +310,8 @@ def search_objects(
except sqlite3.OperationalError: except sqlite3.OperationalError:
like = f"%{query}%" like = f"%{query}%"
sql2 = """ sql2 = """
SELECT config, full_name, object_type, name, synonym, comment, path, '' AS snip, 0 AS score SELECT config, full_name, object_type, name, synonym, comment, path,
id AS object_id, '' AS snip, 0 AS score
FROM objects FROM objects
WHERE full_name LIKE ? OR synonym LIKE ? OR comment LIKE ? OR name LIKE ? WHERE full_name LIKE ? OR synonym LIKE ? OR comment LIKE ? OR name LIKE ?
""" """
@@ -343,7 +347,7 @@ def search_objects(
merged.sort( merged.sort(
key=lambda x: (x.get("_boost", 9), x.get("score") if x.get("score") is not None else 0) key=lambda x: (x.get("_boost", 9), x.get("score") if x.get("score") is not None else 0)
) )
return merged[:limit] return annotate_search_hits(conn, merged[:limit], query)
def search_modules( def search_modules(
@@ -403,7 +407,7 @@ def find_refs_to(
Возвращает (строки, totals_by_config). Возвращает (строки, totals_by_config).
При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую** При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую**
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT). конфигурацию (чтобы одна baseconf не вытесняла другую из-за ORDER BY + LIMIT).
""" """
kind = "" kind = ""
name = target 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('mode', '?')}`, БД: `cache/1c_meta/index.sqlite`.",
f"Потоки: {summary.get('workers', '?')}, время: {summary.get('elapsed_sec', '?')} с.", f"Потоки: {summary.get('workers', '?')}, время: {summary.get('elapsed_sec', '?')} с.",
"", "",
"Поиск: `python tools/index/query_1c.py search \"\"`", "Поиск: `python query_1c.py search \"\"`",
"Переиндексация: `python tools/index/index_1c.py reindex` (инкремент) / `--full`.", "Переиндексация: `python index_1c.py reindex` (инкремент) / `--full`.",
"", "",
"## Конфигурации", "## Конфигурации",
"", "",
+30 -18
View File
@@ -1,20 +1,20 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Переиндексация метаданных и модулей конфигураций 1С → cache/1c_meta/index.sqlite. """Переиндексация метаданных и модулей 1С (конфигурации + внешние обработки) → cache/1c_meta/index.sqlite.
Параметры: Параметры:
--config, -c файл настроек утилиты (JSON) --config, -c файл настроек утилиты (YAML)
--baseconf, -b какие выгрузки 1С индексировать (crm3-26, target, …) --baseconf, -b какие источники 1С индексировать (каталог с src/ или virtual external)
Примеры: Примеры:
python tools/index/index_1c.py reindex -j 12 python index_1c.py reindex -j 12
python tools/index/index_1c.py reindex --full -j 12 python index_1c.py reindex --full -j 12
python tools/index/index_1c.py reindex --full -b crm3-26 -j 12 python index_1c.py reindex --full -b my-config -j 12
python tools/index/index_1c.py reindex -b target -b ws-rhana -t Documents,Catalogs python index_1c.py reindex -b main -b my-config.ext -t Documents,Catalogs
python tools/index/index_1c.py status python index_1c.py status
python tools/index/index_1c.py list python index_1c.py list
Поиск: python tools/index/query_1c.py … Поиск: python query_1c.py …
""" """
from __future__ import annotations from __future__ import annotations
@@ -39,10 +39,11 @@ from index1c.cli import ( # noqa: E402
validate_baseconfs, validate_baseconfs,
) )
from index1c.config import ( # noqa: E402 from index1c.config import ( # noqa: E402
CONFIG_ALIASES,
DEFAULT_SKIP_TYPES, DEFAULT_SKIP_TYPES,
cache_dir, cache_dir,
discover_configs, discover_configs,
get_default_baseconfs,
get_config_aliases,
resolve_object_type, resolve_object_type,
) )
from index1c import db as dbmod # noqa: E402 from index1c import db as dbmod # noqa: E402
@@ -70,7 +71,17 @@ def _select_configs_for_reindex(args: argparse.Namespace, root: Path) -> list:
sys.exit(e.exit_code) sys.exit(e.exit_code)
return discover_configs(root, names) return discover_configs(root, names)
# без -b: все известные с src/ # без -b: default_baseconfs из --config или автосканирование.
defaults = get_default_baseconfs()
if defaults:
try:
names = validate_baseconfs(root, defaults, require_indexed=False)
except BaseconfError as e:
print(str(e), file=sys.stderr)
sys.exit(e.exit_code)
return discover_configs(root, names)
# fallback: все каталоги с src/ в корне проекта
return discover_configs(root, None) return discover_configs(root, None)
@@ -79,8 +90,9 @@ def cmd_reindex(args: argparse.Namespace) -> int:
configs = _select_configs_for_reindex(args, root) configs = _select_configs_for_reindex(args, root)
if not configs: if not configs:
print( print(
"Нет конфигураций для индексации (нужна папка <name>/src/).\n" "Нет источников для индексации (нужна папка <name>/src/ "
"Укажите -b crm3-26 или проверьте: python tools/index/index_1c.py list", "или внешняя обработка <dir>/<name>.xml + <dir>/<name>/).\n"
"Укажите -b <имя-выгрузки> или проверьте: python index_1c.py list",
file=sys.stderr, file=sys.stderr,
) )
return 1 return 1
@@ -150,7 +162,7 @@ def cmd_list_configs(args: argparse.Namespace) -> int:
found = discover_configs(root, None) found = discover_configs(root, None)
print("Выгрузки 1С в репозитории (для --baseconf / -b):") print("Выгрузки 1С в репозитории (для --baseconf / -b):")
for c in found: for c in found:
aliases = [k for k, v in CONFIG_ALIASES.items() if v == c.name] aliases = [k for k, v in get_config_aliases().items() if v == c.name]
alias_s = f" алиасы: {', '.join(aliases)}" if aliases else "" alias_s = f" алиасы: {', '.join(aliases)}" if aliases else ""
print(f"- {c.name}{alias_s}") print(f"- {c.name}{alias_s}")
db_path = dbmod.global_db_path(cache_dir(root)) db_path = dbmod.global_db_path(cache_dir(root))
@@ -176,7 +188,7 @@ def build_parser() -> argparse.ArgumentParser:
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=( epilog=(
"Конфигурация 1С: --baseconf (-b). Файл настроек утилиты: --config (-c).\n" "Конфигурация 1С: --baseconf (-b). Файл настроек утилиты: --config (-c).\n"
"Алиасы baseconf: target→crm3-26, source→crm3-dev" "Алиасы/default для baseconf — в YAML (--config), см. config.example.yml"
), ),
) )
p.add_argument( p.add_argument(
@@ -224,10 +236,10 @@ def build_parser() -> argparse.ArgumentParser:
lc = sub.add_parser( lc = sub.add_parser(
"list", "list",
help="Выгрузки в репозитории и в индексе", help="Источники в репозитории и в индексе",
parents=[tool_parent], parents=[tool_parent],
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
description="Выгрузки 1С с src/ в репозитории и что уже в index.sqlite.", description="Источники 1С в репозитории (src/ и external) и что уже в index.sqlite.",
) )
lc.set_defaults(func=cmd_list_configs) lc.set_defaults(func=cmd_list_configs)
+29 -37
View File
@@ -2,22 +2,19 @@
"""Запросы к единому индексу конфигураций 1С (cache/1c_meta/index.sqlite). """Запросы к единому индексу конфигураций 1С (cache/1c_meta/index.sqlite).
Параметры: Параметры:
--config, -c файл настроек утилиты (JSON), не конфигурация 1С --config, -c файл настроек утилиты (YAML), не конфигурация 1С
--baseconf, -b выгрузка конфигурации 1С в репозитории (crm3-26, target, …) --baseconf, -b выгрузка конфигурации 1С в репозитории (имя каталога с src/)
Примеры: Примеры:
python tools/index/query_1c.py search \"ЗаказКлиента\" python query_1c.py search \"Contract\"
python tools/index/query_1c.py search \"Партнер\" --type Document -b crm3-26 python query_1c.py search \"Partner\" --type Document -b my-config
python tools/index/query_1c.py show ЗаказКлиента -b target python query_1c.py show Document.Contract -b main
python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26 python query_1c.py modules \"ProcessData\" -b my-config.ext
python tools/index/query_1c.py modules \"РВС_Transfer\" -b ws-rhana python query_1c.py refs Catalog.Customers -b main
python tools/index/query_1c.py refs Catalog.Партнеры -b source python query_1c.py movements Document.Contract -b my-config
python tools/index/query_1c.py movements ЗаказКлиента -b target python query_1c.py object Contract -b my-config --clear
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target python query_1c.py module CommonModules/MyModule
python tools/index/query_1c.py object ЗаказКлиента -b target
python tools/index/query_1c.py object Партнер -b crm3-26 --clear
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы
""" """
from __future__ import annotations from __future__ import annotations
@@ -57,9 +54,11 @@ from index1c.search import ( # noqa: E402
search_objects, search_objects,
) )
from index1c.object_view import ( # noqa: E402 from index1c.object_view import ( # noqa: E402
enrich_matched_fields,
find_structure_matches, find_structure_matches,
format_object_tree, format_object_tree,
load_object_structure, load_object_structure,
load_object_structure_for_row,
) )
@@ -94,7 +93,21 @@ def cmd_search(args: argparse.Namespace) -> int:
for r in rows: for r in rows:
syn = f"{r['synonym']}" if r.get("synonym") else "" syn = f"{r['synonym']}" if r.get("synonym") else ""
print(f"- [{r['config']}] `{r['full_name']}`{syn}") print(f"- [{r['config']}] `{r['full_name']}`{syn}")
if r.get("snip"): matched = list(r.get("matched_fields") or [])
if r.get("match_kind") == "field" and matched:
obj = load_object_structure_for_row(conn, r)
enrich_matched_fields(obj, matched)
block = format_object_tree(
obj,
clear=True,
matched_fields=matched,
match_kind="field",
omit_header=True,
)
for line in block.splitlines():
if line:
print(f" {line}")
elif r.get("snip"):
print(f" {r['snip']}") print(f" {r['snip']}")
return 0 return 0
finally: finally:
@@ -286,29 +299,8 @@ def cmd_object(args: argparse.Namespace) -> int:
if i: if i:
print() print()
obj = load_object_structure(m) obj = load_object_structure(m)
# подтянуть FillChecking/типы из свежего XML в matched_fields
matched = list(m.get("matched_fields") or []) matched = list(m.get("matched_fields") or [])
if matched and obj.get("fields"): enrich_matched_fields(obj, matched)
by_f = {
(
f.get("tabular_section") or "",
f.get("name") or "",
f.get("kind") or "",
): f
for f in obj["fields"]
}
for mf in matched:
key = (
mf.get("tabular_section") or "",
mf.get("name") or "",
mf.get("kind") or "",
)
src = by_f.get(key)
if src:
mf["fill_checking"] = src.get("fill_checking") or ""
mf["types"] = src.get("types") or mf.get("types")
mf["synonym"] = src.get("synonym") or mf.get("synonym")
mf["refs"] = src.get("refs") or mf.get("refs")
# точное совпадение объекта (score 0) → clear без дерева; # точное совпадение объекта (score 0) → clear без дерева;
# иначе при найденных реквизитах — clear только по ним # иначе при найденных реквизитах — clear только по ним
kind = m.get("match_kind") or "object" kind = m.get("match_kind") or "object"
@@ -360,7 +352,7 @@ def build_parser() -> argparse.ArgumentParser:
epilog=( epilog=(
"Конфигурация 1С — параметр --baseconf (-b): папка выгрузки в репозитории.\n" "Конфигурация 1С — параметр --baseconf (-b): папка выгрузки в репозитории.\n"
"Можно указать до или после подкоманды; несколько раз или через запятую.\n" "Можно указать до или после подкоманды; несколько раз или через запятую.\n"
"Файл настроек утилиты — --config (-c): см. tools/index/config.example.json" "Файл настроек утилиты — --config (-c): см. config.example.yml"
), ),
) )
p.add_argument( p.add_argument(