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/
.pytest_cache/
config.local.json
config.local.yml
config.local.yaml
+44 -6
View File
@@ -1,10 +1,46 @@
# Changelog
Все значимые изменения **tools/index** (индекс метаданных и модулей 1С) фиксируются в этом файле.
Все значимые изменения утилиты **index** (индекс метаданных и модулей 1С) фиксируются в этом файле.
Формат: [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` теперь учитывают не только `<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
### Changed
@@ -15,7 +51,7 @@
### 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
@@ -48,7 +84,7 @@
### Changed
- Параметр конфигурации 1С: **`--baseconf` / `-b`** (алиасы `target``crm3-26`, `source``crm3-dev`).
- Параметр конфигурации 1С: **`--baseconf` / `-b`**; алиасы через `baseconf_aliases` в `--config`.
- **`--config` / `-c`** — только файл настроек утилиты (JSON), не выгрузка 1С.
- Информативные ошибки: нет индекса / нет папки `src/` / baseconf не проиндексирован / объект не найден (с подсказками).
- Расширена документация `README.md` (примеры, режимы, применение в Cursor).
@@ -67,6 +103,8 @@
- `query_1c.py`: `search`, `show`, `refs`, `modules`, `module`.
- Инкремент по `mtime`+`size` файлов; индекс модулей `**/*.bsl`.
[0.3.0]: https://git.p7net.ru/1c/index/-/tags/v0.3.0
[0.2.0]: https://git.p7net.ru/1c/index/-/tags/v0.2.0
[0.1.0]: https://git.p7net.ru/1c/index/-/tags/v0.1.0
[0.3.2]: v0.3.2
[0.3.1]: v0.3.1
[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С
**Версия:** см. [`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)
**Лицензия:** [GNU GPL v3](LICENSE) · [русский перевод (справочно)](LICENSE.ru)
@@ -11,19 +11,16 @@
| [`index_1c.py`](index_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
python tools/index/index_1c.py --version
python tools/index/query_1c.py --version
python index_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 movements` |
| Найти процедуру/фрагмент в BSL | `query_1c.py modules` |
| Сравнить целевую и исходную конфигурацию | `-b target` / `-b source` |
| Сравнить несколько выгрузок в одном репозитории | `-b cfg1 -b cfg2` |
**Типичный workflow агента:**
**Типичный workflow:**
`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). |
| `--root` | `-r` | Корень проекта CRM3-26 (если запуск не из дерева проекта). |
| `--baseconf` | `-b` | Выгрузка конфигурации 1С: папка `<name>/src/` в репозитории. Можно несколько раз. |
| `--config` | `-c` | Файл **настроек утилиты** (YAML). **Не** конфигурация 1С. См. [`config.example.yml`](config.example.yml). |
| `--root` | `-r` | Корень проекта с выгрузками 1С (если запуск не из дерева проекта). |
| `--baseconf` | `-b` | Источник индексации: `<name>/src/` или virtual внешняя обработка `<dir>.<ИмяОбработки>`. Можно несколько раз. |
| `--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`
| Алиас | Папка |
|-------|-------|
| `target`, `crm3_26` | `crm3-26` |
| `source`, `crm3_old`, `crm3-old`, `crm3_dev` | `crm3-dev` |
Краткие имена задаются в файле `--config`, поле **`baseconf_aliases`**:
Другие выгрузки без алиаса: `crm3-26.rhana`, `crm3-dev.rhana`, `ws-rhana`, `bu-corp`, …
Полный список: `python tools/index/index_1c.py list`
```yaml
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` | обновить индекс |
| `status` | размер БД, meta, список проиндексированных baseconf |
| `list` | выгрузки с `src/` в репозитории + что уже в индексе |
| `list` | источники индексации в репозитории + что уже в индексе |
### Режимы `reindex`
@@ -78,16 +86,17 @@ python tools/index/query_1c.py --version
|-------|-----------|
| `reindex -j 12` | инкремент: только файлы с изменившимися `mtime`/`size` |
| `reindex --full -j 12` | удалить всю БД и собрать заново все известные выгрузки |
| `reindex --full -b crm3-26` | пересобрать только указанную конфигурацию |
| `reindex -b target -t Documents,Catalogs` | только выбранные типы объектов |
| `reindex -b ws-rhana --no-modules` | метаданные без BSL |
| `reindex --full -b my-config` | пересобрать только указанную конфигурацию |
| `reindex -b main -t Documents,Catalogs` | только выбранные типы объектов |
| `reindex -b my-config --no-modules` | метаданные без BSL |
| `reindex --md` | дополнительно Markdown в `cache/1c_meta/md/<baseconf>/` |
`-j` / `--workers` — число потоков (по умолчанию 4–16).
### Что индексируется
- **Метаданные** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений).
- **Метаданные конфигураций/расширений** `src/<Тип>/*.xml`: имя, синоним, комментарий, реквизиты / измерения / ресурсы / ТЧ, типы (в т.ч. `TypeSet`), ссылки, `FillChecking`, `RegisterRecords` (документ → регистры движений).
- **Метаданные внешних обработок**: `<dir>/<ИмяОбработки>.xml` (как `ExternalDataProcessor`).
- **Модули** `**/*.bsl`: путь, owner (`Document.X`), вид модуля, имена процедур/функций, тело (до 200 КБ в FTS).
- **Связи движений** — таблица `register_records` (обратный индекс регистраторов).
@@ -96,17 +105,20 @@ python tools/index/query_1c.py --version
### Примеры переиндексации
```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
python tools/index/index_1c.py list
# внешние обработки: развернуть папку в virtual baseconf
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` |
| `show` | имя объекта | полная карточка JSON | `-b` |
| `refs` | `Catalog.Партнеры` / `ЗаказКлиента` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
| `refs` | `Catalog.Customers` / `Contract` | обратные ссылки полей | `-b`, `--limit` (на каждую baseconf) |
| `movements` | документ или регистр | регистратор ↔ регистры движений | `-b`, `--type`, `--limit` |
| `modules` | строка | поиск по BSL | `-b`, `-n` / `--names-only`, `--limit` |
| `module` | путь или owner | символы модуля, строки | `-b` |
@@ -131,22 +143,22 @@ python tools/index/index_1c.py list
```bash
# полная структура документа
python tools/index/query_1c.py object ЗаказКлиента -b target
python query_1c.py object Contract -b my-config
# только реквизиты с именем/синонимом «Партнер» в документах
python tools/index/query_1c.py object Партнер -b target --type Document --clear
# только реквизиты с именем/синонимом «Partner» в документах
python query_1c.py object Partner -b my-config --type Document --clear
# совпал сам объект → --clear печатает только заголовок
python tools/index/query_1c.py object ЗаказКлиента -b target --clear
python query_1c.py object Contract -b my-config --clear
```
Формат строки реквизита:
```text
├── `Attribute.Партнер` — Клиент [обяз.] : CatalogRef.Партнеры
├── `Attribute.СуммаДокумента` — … [необяз.] : DefinedType.ДенежнаяСуммаЛюбогоЗнака
├── `TabularSection.Товары` — Товары
│ └── `Column.Номенклатура` — … [обяз.] : CatalogRef.Номенклатура
├── `Attribute.Partner` — Клиент [обяз.] : CatalogRef.Customers
├── `Attribute.Amount` — … [необяз.] : DefinedType.MoneyAmount
├── `TabularSection.Lines` — Строки
│ └── `Column.Item` — … [обяз.] : CatalogRef.Items
```
- **[обяз.]** — `FillChecking=ShowError`; **[необяз.]** — `DontCheck`
@@ -156,24 +168,24 @@ python tools/index/query_1c.py object ЗаказКлиента -b target --clear
### `refs` — кто ссылается
```bash
python tools/index/query_1c.py refs "ЗаказКлиента"
python tools/index/query_1c.py refs Catalog.Партнеры -b target --limit 50
python tools/index/query_1c.py refs Document.ЗаказКлиента -b source
python query_1c.py refs "Contract"
python query_1c.py refs Catalog.Customers -b my-config --limit 50
python query_1c.py refs Document.Contract -b main
```
Без `-b` — все конфигурации в индексе; `--limit` действует **на каждую**.
Составной тип поля: `→ DocumentRef.ЗаказКлиента (среди прочего: DocumentRef.…)`.
Составной тип поля: `→ DocumentRef.Contract (среди прочего: DocumentRef.…)`.
### `movements` — регистратор ↔ регистры движений
```bash
# документ → регистры, в которых он регистратор
python tools/index/query_1c.py movements ЗаказКлиента -b target
python tools/index/query_1c.py movements Document.РеализацияТоваровУслуг -b target
python query_1c.py movements Contract -b my-config
python query_1c.py movements Document.Sales -b my-config
# регистр → документы-регистраторы
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target
python tools/index/query_1c.py movements ТоварыНаСкладах -b target --type AccumulationRegister
python query_1c.py movements AccumulationRegister.Stock -b my-config
python query_1c.py movements Stock -b my-config --type AccumulationRegister
```
Данные из свойства метаданных `RegisterRecords` (таблица `register_records` в индексе).
@@ -182,18 +194,18 @@ python tools/index/query_1c.py movements ТоварыНаСкладах -b targe
### `modules` / `module`
```bash
python tools/index/query_1c.py modules "ОбработатьHealth" -b ws-rhana
python tools/index/query_1c.py modules "РВС_Transfer" -b ws-rhana -n
python tools/index/query_1c.py module CommonModules/РВС_TransferAPIДокументы -b ws-rhana
python query_1c.py modules "ProcessData" -b my-config
python query_1c.py modules "MyModule" -b my-config.ext -n
python query_1c.py module CommonModules/MyModule -b my-config
```
### Прочие примеры
```bash
python tools/index/query_1c.py search "ЗаказКлиента"
python tools/index/query_1c.py search "Партнер" --type Document -b crm3-26
python tools/index/query_1c.py show Document.ЗаказКлиента -b target
python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rhana
python query_1c.py search "Contract"
python query_1c.py search "Partner" --type Document -b my-config
python query_1c.py show Document.Contract -b main
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` |
| `-b` без папки `src/` | список доступных выгрузок + алиасы |
| `-b` без папки `src/` | список доступных выгрузок |
| `-b` есть в репо, нет в индексе | список проиндексированных + команда `reindex -b …` |
| `show` / `object` не нашли | похожие имена из индекса |
@@ -210,29 +222,29 @@ python tools/index/query_1c.py search "Партнер" -b source -b crm3-dev.rha
## Файл настроек утилиты (`--config`)
```bash
cp tools/index/config.example.json tools/index/config.local.json
# project_root: "/path/to/crm3-26"
python tools/index/query_1c.py --config tools/index/config.local.json search "Заказ"
cp config.example.yml config.local.yml
# project_root: "/path/to/project"
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/`.
3. Разделять контекст: `-b target` vs `-b source`.
3. При нескольких выгрузках — ограничивать `-b` нужной baseconf.
4. Код: `modules` → открыть `rel_path`; структура документа: `object`.
5. Правило агента: `.cursor/rules/1c-meta-index.mdc`.
---
## Артефакты кэша
```text
cache/1c_meta/ # в корне проекта CRM3-26 (gitignore)
cache/1c_meta/ # в корне проекта (обычно в .gitignore)
index.sqlite # единая БД
INDEX.md
manifest.json
@@ -251,8 +263,6 @@ Per-config `search.sqlite` устарели и не используются.
| **E-mail** | [mk@p7net.ru](mailto: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
__version__ = "0.3.1"
__version__ = "0.3.2"
__status__ = "in development"
__author__ = "Michael BAG"
__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 {}
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)
+96 -23
View File
@@ -3,7 +3,6 @@
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Any
@@ -21,25 +20,89 @@ class BaseconfError(Exception):
def load_tool_config(path: Path) -> dict[str, Any]:
"""Файл настроек утилиты index (JSON). Не путать с конфигурацией 1С."""
"""Файл настроек утилиты index (YAML; legacy JSON поддерживается)."""
ext = path.suffix.lower()
try:
data = json.loads(path.read_text(encoding="utf-8"))
raw = path.read_text(encoding="utf-8")
except OSError as e:
raise BaseconfError(f"Не удалось прочитать --config {path}: {e}", exit_code=1) from e
except json.JSONDecodeError as e:
raise BaseconfError(f"Некорректный JSON в --config {path}: {e}", exit_code=1) from e
if ext == ".json":
try:
import json
data = json.loads(raw)
except Exception as e:
raise BaseconfError(f"Некорректный JSON в --config {path}: {e}", exit_code=1) from e
if not isinstance(data, dict):
raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1)
return data
try:
import yaml # type: ignore
except Exception as e:
raise BaseconfError(
"Для чтения --config в формате YAML нужен пакет pyyaml: pip install pyyaml",
exit_code=1,
) from e
try:
data = yaml.safe_load(raw)
except Exception as e:
raise BaseconfError(f"Некорректный YAML в --config {path}: {e}", exit_code=1) from e
if not isinstance(data, dict):
raise BaseconfError(f"--config {path}: ожидается JSON-объект", exit_code=1)
raise BaseconfError(f"--config {path}: ожидается YAML-объект", exit_code=1)
return data
def discover_local_tool_config() -> Path | None:
"""Автопоиск локального конфига без --config."""
candidates = ("config.local.yml", "config.local.yaml", "config.local.json")
roots = [Path.cwd(), project_root_from_here()]
seen: set[Path] = set()
for root in roots:
for rel in (Path("tools/index"), Path(".")):
base = (root / rel).resolve()
if base in seen or not base.is_dir():
continue
seen.add(base)
for name in candidates:
p = base / name
if p.is_file():
return p
return None
def apply_tool_config(data: dict[str, Any]) -> None:
"""Применить настройки из YAML (--config): алиасы и дефолтные baseconf."""
from .config import set_config_aliases, set_default_baseconfs
aliases = data.get("baseconf_aliases")
if aliases is not None:
if not isinstance(aliases, dict):
raise BaseconfError("--config: baseconf_aliases должен быть объектом YAML")
set_config_aliases({str(k): str(v) for k, v in aliases.items()})
default_baseconfs = data.get("default_baseconfs")
if default_baseconfs is not None:
if not isinstance(default_baseconfs, list):
raise BaseconfError("--config: default_baseconfs должен быть YAML-списком")
set_default_baseconfs([str(x) for x in default_baseconfs])
else:
set_default_baseconfs([])
def resolve_project_root(args: argparse.Namespace) -> Path:
"""Корень проекта: --root → project_root из --config → авто."""
"""Корень проекта: --root → --config/auto-config → авто."""
if getattr(args, "root", None):
return Path(args.root).resolve()
cfg_path = getattr(args, "config", None)
if not cfg_path:
auto = discover_local_tool_config()
if auto is not None:
cfg_path = str(auto)
setattr(args, "config", cfg_path)
if cfg_path:
data = load_tool_config(Path(cfg_path))
apply_tool_config(data)
pr = data.get("project_root")
if pr:
return Path(pr).resolve()
@@ -52,13 +115,16 @@ def add_tool_args(parser: argparse.ArgumentParser) -> None:
"--config",
"-c",
metavar="FILE",
help="JSON с настройками утилиты (project_root и др.), см. config.example.json",
help=(
"YAML с настройками утилиты (project_root и др.), см. config.example.yml. "
"Без параметра: автопоиск config.local.yml/.yaml/.json"
),
)
parser.add_argument(
"--root",
"-r",
metavar="DIR",
help="Корень проекта CRM3-26 (перекрывает project_root из --config)",
help="Корень проекта с выгрузками 1С (перекрывает project_root из --config)",
)
@@ -69,7 +135,10 @@ def add_baseconf_args(
) -> None:
"""Фильтр по выгрузке конфигурации 1С в репозитории."""
if mode == "index":
default_hint = "Без параметра — индексировать все выгрузки с src/ в репозитории."
default_hint = (
"Без параметра — default_baseconfs из --config; "
"если блок не задан, индексировать все выгрузки с src/ и внешние обработки."
)
else:
default_hint = "Без параметра — поиск по всем проиндексированным."
parser.add_argument(
@@ -79,9 +148,9 @@ def add_baseconf_args(
metavar="NAME",
dest="baseconf",
help=(
"Конфигурация 1С в репозитории (папка <name>/src/). "
"Можно несколько раз или через запятую: -b target,source. "
"Алиасы: target→crm3-26, source→crm3-dev. "
"Источник индексации 1С: <name>/src/ или virtual <dir>.<Обработка>. "
"Можно несколько раз или через запятую: -b cfg1,cfg2. "
"Краткие имена (алиасы) — в baseconf_aliases файла --config. "
f"{default_hint}"
),
)
@@ -165,34 +234,38 @@ 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(
[
"",
"Алиасы: target→crm3-26, source→crm3-dev, crm3_old→crm3-dev",
"Краткие имена (алиасы) для -b задаются в baseconf_aliases файла --config.",
]
)
raise BaseconfError("\n".join(lines))
@@ -216,7 +289,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 +370,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 <имя-выгрузки>"
)
+162 -27
View File
@@ -2,31 +2,14 @@
from __future__ import annotations
import re
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] = {
@@ -71,6 +54,7 @@ OBJECT_TYPE_FOLDERS: dict[str, str] = {
"XDTOPackages": "XDTOPackage",
"Bots": "Bot",
"ExternalDataSources": "ExternalDataSource",
"ExternalDataProcessors": "ExternalDataProcessor",
"Languages": "Language",
}
@@ -110,6 +94,10 @@ TYPE_ALIASES: dict[str, str] = {
"dataprocessors": "DataProcessor",
"обработка": "DataProcessor",
"обработки": "DataProcessor",
"externaldataprocessor": "ExternalDataProcessor",
"externaldataprocessors": "ExternalDataProcessor",
"внешняяобработка": "ExternalDataProcessor",
"внешниеобработки": "ExternalDataProcessor",
"report": "Report",
"reports": "Report",
"отчет": "Report",
@@ -189,16 +177,105 @@ class ConfigPaths:
name: str
root: Path
src: Path
source_kind: str = "src" # src | external_processor
external_name: str = ""
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]:
"""Все источники индексации в корне проекта.
Возвращает:
- обычные конфигурации/расширения: <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:
@@ -217,13 +294,71 @@ 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] = []
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
+1 -1
View File
@@ -245,7 +245,7 @@ def init_db(conn: sqlite3.Connection) -> None:
if ver > SCHEMA_VERSION:
raise RuntimeError(
f"Индекс новее утилиты (схема {ver}, утилита {SCHEMA_VERSION}). "
"Обновите tools/index."
"Обновите утилиту index до более новой версии."
)
if ver < SCHEMA_VERSION:
migrate_schema(conn, ver)
+205 -24
View File
@@ -3,12 +3,15 @@
from __future__ import annotations
import json
import re
import sqlite3
from pathlib import Path
from typing import Any
from .parse_meta import FieldRec, ObjectRec, parse_metadata_xml
_QUERY_TOKEN_RE = re.compile(r"[\wА-Яа-яЁё]+", flags=re.UNICODE)
FILL_LABEL = {
"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
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(
conn: sqlite3.Connection,
query: str,
@@ -239,12 +410,43 @@ def format_field_line(
return f"{indent}{branch}`{kind}.{name}`{syn_s} [{fill}] : {types_s}"
def _select_matched_field_nodes(
fields: list[dict[str, Any]],
matched_fields: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], set[tuple[str, str, str]], set[str]]:
want: set[tuple[str, str, str]] = set()
need_ts: set[str] = set()
for mf in matched_fields:
want.add(
(
mf.get("kind") or "",
mf.get("tabular_section") or "",
mf.get("name") or "",
)
)
if mf.get("tabular_section") and mf.get("kind") == "Column":
need_ts.add(mf["tabular_section"])
selected: list[dict[str, Any]] = []
for f in fields:
key = (
f.get("kind") or "",
f.get("tabular_section") or "",
f.get("name") or "",
)
if key in want:
selected.append(f)
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
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] = []
@@ -254,35 +456,14 @@ def format_object_tree(
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)
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:
want: set[tuple[str, str, str]] = set()
need_ts: set[str] = set()
for mf in matched_fields:
want.add(
(
mf.get("kind") or "",
mf.get("tabular_section") or "",
mf.get("name") or "",
)
)
if mf.get("tabular_section") and mf.get("kind") == "Column":
need_ts.add(mf["tabular_section"])
selected: list[dict[str, Any]] = []
for f in fields:
key = (
f.get("kind") or "",
f.get("tabular_section") or "",
f.get("name") or "",
)
if key in want:
selected.append(f)
elif f.get("kind") == "TabularSection" and f.get("name") in need_ts:
selected.append(f)
selected, want, need_ts = _select_matched_field_nodes(fields, matched_fields)
if not selected:
lines.append(" (совпавшие реквизиты не найдены в структуре)")
return "\n".join(lines)
+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:
+9 -5
View File
@@ -10,6 +10,7 @@ from typing import Any
from . import db as dbmod
from .config import cache_dir
from .object_view import annotate_search_hits
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))
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)
@@ -266,7 +267,8 @@ def search_objects(
limit: int = 30,
) -> list[dict[str, Any]]:
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
WHERE (name = ? OR full_name = ? OR full_name LIKE ?)
"""
@@ -286,6 +288,7 @@ def search_objects(
fetch_n = max(limit * 5, 50)
sql = """
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,
bm25(objects_fts, 10.0, 5.0, 2.0, 1.0) AS score
FROM objects_fts
@@ -307,7 +310,8 @@ def search_objects(
except sqlite3.OperationalError:
like = f"%{query}%"
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
WHERE full_name LIKE ? OR synonym LIKE ? OR comment LIKE ? OR name LIKE ?
"""
@@ -343,7 +347,7 @@ def search_objects(
merged.sort(
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(
@@ -403,7 +407,7 @@ def find_refs_to(
Возвращает (строки, totals_by_config).
При поиске по нескольким конфигурациям ``limit`` — максимум **на каждую**
конфигурацию (чтобы crm3-26 не вытеснял crm3-dev из-за ORDER BY + LIMIT).
конфигурацию (чтобы одна baseconf не вытесняла другую из-за ORDER BY + LIMIT).
"""
kind = ""
name = target
+2 -2
View File
@@ -272,8 +272,8 @@ def write_global_index_md(cache_root: Path, summary: dict[str, Any]) -> Path:
f"Режим: `{summary.get('mode', '?')}`, БД: `cache/1c_meta/index.sqlite`.",
f"Потоки: {summary.get('workers', '?')}, время: {summary.get('elapsed_sec', '?')} с.",
"",
"Поиск: `python tools/index/query_1c.py search \"\"`",
"Переиндексация: `python tools/index/index_1c.py reindex` (инкремент) / `--full`.",
"Поиск: `python query_1c.py search \"\"`",
"Переиндексация: `python index_1c.py reindex` (инкремент) / `--full`.",
"",
"## Конфигурации",
"",
+30 -18
View File
@@ -1,20 +1,20 @@
#!/usr/bin/env python3
"""Переиндексация метаданных и модулей конфигураций 1С → cache/1c_meta/index.sqlite.
"""Переиндексация метаданных и модулей 1С (конфигурации + внешние обработки) → cache/1c_meta/index.sqlite.
Параметры:
--config, -c файл настроек утилиты (JSON)
--baseconf, -b какие выгрузки 1С индексировать (crm3-26, target, )
--config, -c файл настроек утилиты (YAML)
--baseconf, -b какие источники 1С индексировать (каталог с src/ или virtual external)
Примеры:
python tools/index/index_1c.py reindex -j 12
python tools/index/index_1c.py reindex --full -j 12
python tools/index/index_1c.py reindex --full -b crm3-26 -j 12
python tools/index/index_1c.py reindex -b target -b ws-rhana -t Documents,Catalogs
python tools/index/index_1c.py status
python tools/index/index_1c.py list
python index_1c.py reindex -j 12
python index_1c.py reindex --full -j 12
python index_1c.py reindex --full -b my-config -j 12
python index_1c.py reindex -b main -b my-config.ext -t Documents,Catalogs
python index_1c.py status
python index_1c.py list
Поиск: python tools/index/query_1c.py
Поиск: python query_1c.py
"""
from __future__ import annotations
@@ -39,10 +39,11 @@ from index1c.cli import ( # noqa: E402
validate_baseconfs,
)
from index1c.config import ( # noqa: E402
CONFIG_ALIASES,
DEFAULT_SKIP_TYPES,
cache_dir,
discover_configs,
get_default_baseconfs,
get_config_aliases,
resolve_object_type,
)
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)
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)
@@ -79,8 +90,9 @@ def cmd_reindex(args: argparse.Namespace) -> int:
configs = _select_configs_for_reindex(args, root)
if not configs:
print(
"Нет конфигураций для индексации (нужна папка <name>/src/).\n"
"Укажите -b crm3-26 или проверьте: python tools/index/index_1c.py list",
"Нет источников для индексации (нужна папка <name>/src/ "
"или внешняя обработка <dir>/<name>.xml + <dir>/<name>/).\n"
"Укажите -b <имя-выгрузки> или проверьте: python index_1c.py list",
file=sys.stderr,
)
return 1
@@ -150,7 +162,7 @@ def cmd_list_configs(args: argparse.Namespace) -> int:
found = discover_configs(root, None)
print("Выгрузки 1С в репозитории (для --baseconf / -b):")
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 ""
print(f"- {c.name}{alias_s}")
db_path = dbmod.global_db_path(cache_dir(root))
@@ -176,7 +188,7 @@ def build_parser() -> argparse.ArgumentParser:
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Конфигурация 1С: --baseconf (-b). Файл настроек утилиты: --config (-c).\n"
"Алиасы baseconf: target→crm3-26, source→crm3-dev"
"Алиасы/default для baseconf — в YAML (--config), см. config.example.yml"
),
)
p.add_argument(
@@ -224,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)
+29 -37
View File
@@ -2,22 +2,19 @@
"""Запросы к единому индексу конфигураций 1С (cache/1c_meta/index.sqlite).
Параметры:
--config, -c файл настроек утилиты (JSON), не конфигурация 1С
--baseconf, -b выгрузка конфигурации 1С в репозитории (crm3-26, target, )
--config, -c файл настроек утилиты (YAML), не конфигурация 1С
--baseconf, -b выгрузка конфигурации 1С в репозитории (имя каталога с src/)
Примеры:
python tools/index/query_1c.py search \"ЗаказКлиента\"
python tools/index/query_1c.py search \"Партнер\" --type Document -b crm3-26
python tools/index/query_1c.py show ЗаказКлиента -b target
python tools/index/query_1c.py show Document.ЗаказКлиента -b crm3-26
python tools/index/query_1c.py modules \"РВС_Transfer\" -b ws-rhana
python tools/index/query_1c.py refs Catalog.Партнеры -b source
python tools/index/query_1c.py movements ЗаказКлиента -b target
python tools/index/query_1c.py movements AccumulationRegister.ТоварыНаСкладах -b target
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Документы
python query_1c.py search \"Contract\"
python query_1c.py search \"Partner\" --type Document -b my-config
python query_1c.py show Document.Contract -b main
python query_1c.py modules \"ProcessData\" -b my-config.ext
python query_1c.py refs Catalog.Customers -b main
python query_1c.py movements Document.Contract -b my-config
python query_1c.py object Contract -b my-config --clear
python query_1c.py module CommonModules/MyModule
"""
from __future__ import annotations
@@ -57,9 +54,11 @@ from index1c.search import ( # noqa: E402
search_objects,
)
from index1c.object_view import ( # noqa: E402
enrich_matched_fields,
find_structure_matches,
format_object_tree,
load_object_structure,
load_object_structure_for_row,
)
@@ -94,7 +93,21 @@ def cmd_search(args: argparse.Namespace) -> int:
for r in rows:
syn = f"{r['synonym']}" if r.get("synonym") else ""
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']}")
return 0
finally:
@@ -286,29 +299,8 @@ def cmd_object(args: argparse.Namespace) -> int:
if i:
print()
obj = load_object_structure(m)
# подтянуть FillChecking/типы из свежего XML в matched_fields
matched = list(m.get("matched_fields") or [])
if matched and obj.get("fields"):
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")
enrich_matched_fields(obj, matched)
# точное совпадение объекта (score 0) → clear без дерева;
# иначе при найденных реквизитах — clear только по ним
kind = m.get("match_kind") or "object"
@@ -360,7 +352,7 @@ def build_parser() -> argparse.ArgumentParser:
epilog=(
"Конфигурация 1С — параметр --baseconf (-b): папка выгрузки в репозитории.\n"
"Можно указать до или после подкоманды; несколько раз или через запятую.\n"
"Файл настроек утилиты — --config (-c): см. tools/index/config.example.json"
"Файл настроек утилиты — --config (-c): см. config.example.yml"
),
)
p.add_argument(