cdc-1c 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. cdc_1c-0.1.0/.github/workflows/ci.yml +37 -0
  2. cdc_1c-0.1.0/.gitignore +215 -0
  3. cdc_1c-0.1.0/CHANGELOG.md +23 -0
  4. cdc_1c-0.1.0/LICENSE +21 -0
  5. cdc_1c-0.1.0/PKG-INFO +200 -0
  6. cdc_1c-0.1.0/PLAN.md +124 -0
  7. cdc_1c-0.1.0/README.md +163 -0
  8. cdc_1c-0.1.0/cdc-1c-form-module.txt +690 -0
  9. cdc_1c-0.1.0/cdc-1c.epf +0 -0
  10. cdc_1c-0.1.0/main.py +46 -0
  11. cdc_1c-0.1.0/materialize_example.py +57 -0
  12. cdc_1c-0.1.0/materialize_example_other_key.py +75 -0
  13. cdc_1c-0.1.0/pyproject.toml +74 -0
  14. cdc_1c-0.1.0/src/cdc_1c/__init__.py +18 -0
  15. cdc_1c-0.1.0/src/cdc_1c/__main__.py +30 -0
  16. cdc_1c-0.1.0/src/cdc_1c/change_reader.py +77 -0
  17. cdc_1c-0.1.0/src/cdc_1c/common_functions.py +30 -0
  18. cdc_1c-0.1.0/src/cdc_1c/config.py +41 -0
  19. cdc_1c-0.1.0/src/cdc_1c/data_reader.py +452 -0
  20. cdc_1c-0.1.0/src/cdc_1c/db_logs.py +103 -0
  21. cdc_1c-0.1.0/src/cdc_1c/db_writer.py +180 -0
  22. cdc_1c-0.1.0/src/cdc_1c/logging_config.py +23 -0
  23. cdc_1c-0.1.0/src/cdc_1c/metadata_reader.py +388 -0
  24. cdc_1c-0.1.0/src/cdc_1c/name_mapper.py +94 -0
  25. cdc_1c-0.1.0/src/cdc_1c/py.typed +0 -0
  26. cdc_1c-0.1.0/src/cdc_1c/replicator.py +377 -0
  27. cdc_1c-0.1.0/tests/env_latest.sh +6 -0
  28. cdc_1c-0.1.0/tests/fake_1c.py +130 -0
  29. cdc_1c-0.1.0/tests/record_1c.py +135 -0
  30. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/manifest.json +12 -0
  31. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg001_metadata.xml +6964 -0
  32. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg002_nomenklatura.xml +664 -0
  33. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg003_zakaz_klienta.xml +2113 -0
  34. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg004_kontragenti.xml +322 -0
  35. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg005_zakaz_klienta_2.xml +243 -0
  36. cdc_1c-0.1.0/tests/responses/trade_demo_8.5/msg006_realizatsiya.xml +2741 -0
  37. cdc_1c-0.1.0/tests/test_cdc_run_once.py +58 -0
  38. cdc_1c-0.1.0/tests/test_entrypoint.py +67 -0
  39. cdc_1c-0.1.0/tests/test_full_load.py +323 -0
  40. cdc_1c-0.1.0/tests/test_json_export.py +101 -0
  41. cdc_1c-0.1.0/tests/test_metadata_objects.py +93 -0
  42. cdc_1c-0.1.0/tests/test_pipeline_replay.py +82 -0
  43. cdc_1c-0.1.0/tests/test_run_forever_live.py +44 -0
  44. cdc_1c-0.1.0/tests/test_version_guard.py +118 -0
  45. cdc_1c-0.1.0/uv.lock +622 -0
@@ -0,0 +1,37 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ["v*"]
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: astral-sh/setup-uv@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ - run: uv sync --extra dev
21
+ # integration-тесты (живая 1С/Postgres) отсеиваются addopts из pyproject.
22
+ - run: uv run pytest -q
23
+
24
+ publish:
25
+ # Публикация на PyPI по git-тегу vX.Y.Z. Требует настроенного Trusted Publishing на стороне PyPI
26
+ # (проект + этот репозиторий/workflow) — секреты/токены не нужны.
27
+ if: startsWith(github.ref, 'refs/tags/v')
28
+ needs: test
29
+ runs-on: ubuntu-latest
30
+ environment: pypi
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: astral-sh/setup-uv@v5
36
+ - run: uv build
37
+ - run: uv publish --trusted-publishing always
@@ -0,0 +1,215 @@
1
+ # Claude Code local settings
2
+ .claude/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[codz]
7
+ *$py.class
8
+
9
+
10
+
11
+ .vscode
12
+ .qwen
13
+
14
+ # C extensions
15
+ *.so
16
+
17
+ # Distribution / packaging
18
+ .Python
19
+ build/
20
+ develop-eggs/
21
+ dist/
22
+ downloads/
23
+ eggs/
24
+ .eggs/
25
+ lib/
26
+ lib64/
27
+ parts/
28
+ sdist/
29
+ var/
30
+ wheels/
31
+ share/python-wheels/
32
+ *.egg-info/
33
+ .installed.cfg
34
+ *.egg
35
+ MANIFEST
36
+
37
+ # PyInstaller
38
+ # Usually these files are written by a python script from a template
39
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
40
+ *.manifest
41
+ *.spec
42
+
43
+ # Installer logs
44
+ pip-log.txt
45
+ pip-delete-this-directory.txt
46
+
47
+ # Unit test / coverage reports
48
+ htmlcov/
49
+ .tox/
50
+ .nox/
51
+ .coverage
52
+ .coverage.*
53
+ .cache
54
+ nosetests.xml
55
+ coverage.xml
56
+ *.cover
57
+ *.py.cover
58
+ .hypothesis/
59
+ .pytest_cache/
60
+ cover/
61
+
62
+ # Translations
63
+ *.mo
64
+ *.pot
65
+
66
+ # Django stuff:
67
+ *.log
68
+ local_settings.py
69
+ db.sqlite3
70
+ db.sqlite3-journal
71
+
72
+ # Flask stuff:
73
+ instance/
74
+ .webassets-cache
75
+
76
+ # Scrapy stuff:
77
+ .scrapy
78
+
79
+ # Sphinx documentation
80
+ docs/_build/
81
+
82
+ # PyBuilder
83
+ .pybuilder/
84
+ target/
85
+
86
+ # Jupyter Notebook
87
+ .ipynb_checkpoints
88
+
89
+ # IPython
90
+ profile_default/
91
+ ipython_config.py
92
+
93
+ # pyenv
94
+ # For a library or package, you might want to ignore these files since the code is
95
+ # intended to run in multiple environments; otherwise, check them in:
96
+ # .python-version
97
+
98
+ # pipenv
99
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
100
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
101
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
102
+ # install all needed dependencies.
103
+ #Pipfile.lock
104
+
105
+ # UV
106
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
107
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
108
+ # commonly ignored for libraries.
109
+ #uv.lock
110
+
111
+ # poetry
112
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
113
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
114
+ # commonly ignored for libraries.
115
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
116
+ #poetry.lock
117
+ #poetry.toml
118
+
119
+ # pdm
120
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
121
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
122
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
123
+ #pdm.lock
124
+ #pdm.toml
125
+ .pdm-python
126
+ .pdm-build/
127
+
128
+ # pixi
129
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
130
+ #pixi.lock
131
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
132
+ # in the .venv directory. It is recommended not to include this directory in version control.
133
+ .pixi
134
+
135
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
136
+ __pypackages__/
137
+
138
+ # Celery stuff
139
+ celerybeat-schedule
140
+ celerybeat.pid
141
+
142
+ # SageMath parsed files
143
+ *.sage.py
144
+
145
+ # Environments
146
+ .env
147
+ .envrc
148
+ .venv
149
+ env/
150
+ venv/
151
+ ENV/
152
+ env.bak/
153
+ venv.bak/
154
+
155
+ # Spyder project settings
156
+ .spyderproject
157
+ .spyproject
158
+
159
+ # Rope project settings
160
+ .ropeproject
161
+
162
+ # mkdocs documentation
163
+ /site
164
+
165
+ # mypy
166
+ .mypy_cache/
167
+ .dmypy.json
168
+ dmypy.json
169
+
170
+ # Pyre type checker
171
+ .pyre/
172
+
173
+ # pytype static type analyzer
174
+ .pytype/
175
+
176
+ # Cython debug symbols
177
+ cython_debug/
178
+
179
+ # PyCharm
180
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
181
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
182
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
183
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
184
+ #.idea/
185
+
186
+ # Abstra
187
+ # Abstra is an AI-powered process automation framework.
188
+ # Ignore directories containing user credentials, local state, and settings.
189
+ # Learn more at https://abstra.io/docs
190
+ .abstra/
191
+
192
+ # Visual Studio Code
193
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
194
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
195
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
196
+ # you could uncomment the following to ignore the entire vscode folder
197
+ # .vscode/
198
+
199
+ # Ruff stuff:
200
+ .ruff_cache/
201
+
202
+ # PyPI configuration file
203
+ .pypirc
204
+
205
+ # Cursor
206
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
207
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
208
+ # refer to https://docs.cursor.com/context/ignore-files
209
+ .cursorignore
210
+ .cursorindexingignore
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ Формат — [Keep a Changelog](https://keepachangelog.com/ru/1.1.0/),
4
+ версионирование — [SemVer](https://semver.org/lang/ru/).
5
+
6
+ ## [0.1.0] — Unreleased
7
+
8
+ Первый публичный релиз (Development Status :: Alpha).
9
+
10
+ ### Added
11
+ - Оркестратор `Replicator1C`: чтение изменений из 1С (OData + план обмена) и upsert в целевую БД
12
+ через `dbmerge`; подтверждение приёма пакета только после успешного сохранения (`run_once` /
13
+ `run_forever` с graceful SIGTERM/SIGINT).
14
+ - Полная выгрузка `full_load`: keyset-пагинация (Ref_Key / Recorder / составной ключ независимого
15
+ регистра), фильтр по периоду (`date_field` + `date_from`/`date_to`), фоновые выгрузки в
16
+ `run_forever`.
17
+ - Version-guard полной выгрузки (`exchange_message_no`): устаревший снимок не затирает более свежие
18
+ изменения и не воскрешает удалённые строки групп (регистр/табличная часть).
19
+ - Транслитерация имён и лимиты идентификаторов (`NameMapper1C`); спец-поля `merged_on`,
20
+ `inserted_on`, `is_deleted_or_empty`, `exchange_message_no`.
21
+ - Служебные таблицы: журнал загрузок `replicator_1c_log`, реестр объектов `metadata_objects_1c`.
22
+
23
+ [0.1.0]: https://github.com/pavel-v-sobolev/cdc_1C/releases/tag/v0.1.0
cdc_1c-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pavel Sobolev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cdc_1c-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: cdc-1c
3
+ Version: 0.1.0
4
+ Summary: Change data capture (CDC) from 1C:Enterprise to your data warehouse
5
+ Project-URL: Homepage, https://github.com/pavel-v-sobolev/cdc_1C
6
+ Project-URL: Repository, https://github.com/pavel-v-sobolev/cdc_1C
7
+ Project-URL: Issues, https://github.com/pavel-v-sobolev/cdc_1C/issues
8
+ Author-email: Pavel Sobolev <pavel-v-sobolev@yandex.ru>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: 1c,1c-enterprise,cdc,data-engineering,dwh,etl,postgres,python
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Topic :: Database
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Requires-Python: >=3.10
28
+ Requires-Dist: dbmerge>=1.0.20
29
+ Requires-Dist: requests>=2.33.0
30
+ Requires-Dist: sqlalchemy>=2.0.49
31
+ Requires-Dist: xmltodict>=1.0.4
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest>=8; extra == 'dev'
34
+ Provides-Extra: postgres
35
+ Requires-Dist: psycopg2-binary>=2.9.12; extra == 'postgres'
36
+ Description-Content-Type: text/markdown
37
+
38
+ **cdc-1c** is a docker container and a Python library, that provides 1C system data loading to data warehouse using Change Data Capture apporach. \
39
+ It engages standard ODATA mechanism and standard 1C exchange plan mechanism to extract data from 1C system and upsert changes to the target DB.
40
+
41
+ **cdc-1c** - это докер контейнер и python-библиотека, предназначенные для получения данных из 1С, использующий подход CDC (загрузка изменений данных). \
42
+ Продукт использует стандартный интерфейс ODATA и механизм планов обмена для выгрузки изменений данных из системы 1С и обновления данных в целевой БД.
43
+
44
+ # Что нужно для работы
45
+ 1) опубликовать базу 1с на web
46
+ 2) создать пользователя odata и дать ему необходимые права
47
+ 3) настроить план обмена в конфигураторе и включить в его состав нужные объекты 1с
48
+ 3) настроить узел обмена с использованием внешней обработки `cdc-1c.odt`
49
+ 4) поднять docker контейнер (плока не доступен - todo) для получения данных или адаптировать под свой вариант, исспользуя библиотеку python cdc-1c
50
+
51
+ # Использование библиотеки python
52
+
53
+ Библиотека даёт оркестратор `Replicator1C`, который читает изменения из 1С (OData + план обмена) и
54
+ складывает их в целевую БД, подтверждая приём пакета только после успешного сохранения. БД
55
+ передаётся готовым SQLAlchemy `engine`.
56
+
57
+ ## Установка
58
+
59
+ ```bash
60
+ pip install cdc-1c
61
+
62
+ # c драйвером PostgreSQL (тестовая СУБД):
63
+ pip install "cdc-1c[postgres]"
64
+ ```
65
+
66
+ Поддерживается Python 3.10+. Модуль тестировался на PostgreSQL, но целевой может быть любая БД из
67
+ числа поддерживаемых модулем `dbmerge` (через него идёт запись). Сама библиотека драйвер БД не
68
+ импортирует — его ставите под свою СУБД (для PostgreSQL — extra `[postgres]` выше).
69
+
70
+ ## Быстрый старт
71
+
72
+ ```python
73
+ from sqlalchemy import create_engine
74
+ from cdc_1c import Replicator1C
75
+
76
+ engine = create_engine("postgresql+psycopg2://user:pass@localhost:5432/dwh", pool_size=5)
77
+
78
+ rep = Replicator1C(
79
+ odata_url="http://host/base/odata/standard.odata",
80
+ odata_auth=("odata", "secret"), # (user, password) либо None без авторизации
81
+ exchange_name="ДляODATA", # имя плана обмена в 1С
82
+ queue_guid="a9bc23c5-3689-11f1-926c-0800270bc6cb", # Ref_Key узла обмена (очереди)
83
+ engine=engine,
84
+ db_schema="cdc_1c", # None → схема БД по умолчанию (public у Postgres)
85
+ request_timeout=60, # таймаут HTTP-запросов к 1С, сек (по умолчанию = interval)
86
+ full_load_workers=2, # число фоновых потоков полной выгрузки
87
+ )
88
+
89
+ rep.run_forever(interval=60) # цикл опроса раз в 60 секунд
90
+ ```
91
+
92
+
93
+ ## Режимы: `run_once` и `run_forever`
94
+
95
+ ```python
96
+ rep.run_once() # один цикл: read → save → notify (подтверждение только после save)
97
+ rep.run_forever(interval=60) # бесконечный цикл run_once с паузой; фоном — полные выгрузки
98
+ ```
99
+
100
+ - `run_once(notify_changes=False)` — не подтверждать приём (пакет останется в очереди 1С; сделано для отладки).
101
+ - `run_forever(interval, max_iterations=0)` — `max_iterations>0` ограничивает число итераций.
102
+
103
+ ## Полная (первоначальная) выгрузка
104
+
105
+ При работе `run_forever` объекты, впервые встреченные в пакете изменений, автоматически ставятся в
106
+ очередь на полную выгрузку и грузятся фоновыми потоками. Можно запустить выгрузку и вручную:
107
+
108
+ Полная выгрузка объекта реализована на стороне python, чтобы поддержать выгрузку объектов больших размеров,
109
+ т.к. если инициировать полную выгрузку по плану обмена в 1С, то данные поступят без возможности постраничной загрузки.
110
+ (Поэтому данная функция специально убрана из модуля 1С).
111
+
112
+ Полная выгрузка спроектирована так, чтобы работать параллельно с получением изменений объекта.
113
+
114
+ ```python
115
+ rep.list_objects() # имена объектов 1С, доступных для выгрузки (Catalog_…, Document_…, …)
116
+
117
+ rep.full_load("Catalog_Номенклатура")
118
+ rep.full_load("Document_РеализацияТоваровУслуг", batch_size=500)
119
+ ```
120
+
121
+
122
+ ### Фильтр по периоду
123
+
124
+ Для ручной догрузки за нужный период укажите поле даты/времени и границы (включительно):
125
+
126
+ ```python
127
+ from datetime import date, datetime
128
+
129
+ # весь месяц: date-граница включает последний день целиком (даже для поля дата-время)
130
+ rep.full_load("Document_РеализацияТоваровУслуг",
131
+ date_field="Date", date_from=date(2026, 6, 1), date_to=date(2026, 6, 30))
132
+
133
+ # точная граница по времени — передайте datetime
134
+ rep.full_load("Document_РеализацияТоваровУслуг",
135
+ date_field="Date", date_from=datetime(2026, 6, 1, 9, 0, 0))
136
+ ```
137
+
138
+ `date_field` — имя поля 1С (`Date` у документов, `Period` у регистров). Границы транслируются в OData
139
+ `$filter` и объединяются с курсором пагинации.
140
+
141
+ ## Что появляется в целевой БД
142
+
143
+ - На каждый объект 1С — таблица (имя транслитерируется, длинные имена усекаются с хэшем под лимит СУБД).
144
+ - Служебные поля строк: `merged_on`/`inserted_on` (момент merge/первой вставки), `is_deleted_or_empty`
145
+ (пометка удаления/пустой набор), `exchange_message_no` (номер пакета обмена).
146
+ - Служебные таблицы `replicator_1c_log` и `metadata_objects_1c` (см. ниже).
147
+
148
+ ## Служебные таблицы
149
+
150
+ ### `replicator_1c_log` — журнал загрузок
151
+
152
+ Строка на каждую загрузку объекта: пакет изменений или полная выгрузка.
153
+
154
+ | Колонка | Назначение |
155
+ |---|---|
156
+ | `id` | суррогатный ключ |
157
+ | `exchange` | имя плана обмена |
158
+ | `object` | имя объекта 1С |
159
+ | `type` | `changes` (пакет изменений) или `full` (полная выгрузка) |
160
+ | `message_no` | номер пакета обмена; `NULL` для полной выгрузки |
161
+ | `started_at` / `finished_at` | начало и конец загрузки; `finished_at IS NULL` — не завершена (упала) |
162
+ | `inserted_row_count` / `updated_row_count` / `deleted_row_count` | счётчики строк merge |
163
+ | `total_time` | суммарное время merge, сек |
164
+
165
+ Предназначено для мониторинга: незавершённые строки (`finished_at IS NULL`) — упавшие загрузки; по `type` и
166
+ `object` видно, что и когда грузилось.
167
+
168
+ ### `metadata_objects_1c` — реестр объектов и состояние полной выгрузки
169
+
170
+ Синхронизируется с метаданными 1С — строка на каждый объект, встреченный в обмене. Ключ таблицы — полное
171
+ имя объекта (регистр и документ могут иметь одинаковое короткое имя).
172
+
173
+ | Колонка | Назначение |
174
+ |---|---|
175
+ | `object_full_name` | полное имя объекта 1С (ключ), например `Catalog_Номенклатура` |
176
+ | `object_full_name_en` | транслит = имя таблицы объекта в БД |
177
+ | `object_name` / `object_type` | имя и тип объекта (`Catalog` / `Document` / `AccumulationRegister` / …) |
178
+ | `fields` / `fields_en` | список полей объекта: имена 1С и их транслит (= колонки в БД) |
179
+ | `full_load_is_required` | объект ожидает полной выгрузки |
180
+ | `last_full_load_dt` | когда объект был полностью выгружен; `NULL` — ни разу |
181
+ | `merged_on` | момент синхронизации записи реестра |
182
+
183
+ Новый объект оркестратор помечает `full_load_is_required=true`, фоновый воркер выгружает его целиком и
184
+ проставляет `last_full_load_dt`. Отсюда же удобно посмотреть список доступных объектов и имена их таблиц.
185
+
186
+ ## Логирование
187
+
188
+ Из коробки библиотека вешает вывод на логгер `cdc_1c` (INFO), если приложение не настроило логирование
189
+ само. Настроили своё — библиотека молчит и пишет через стандартный `logging`.
190
+
191
+ ## Дальнейшая материализация и сборка денормализованных таблиц
192
+
193
+ 1С хранит данные в нормализованном виде. Это значит, что чтобы дотянуться, например,
194
+ из регистра заказов до кода товара, нужно делать JOIN с таблицей номенклатуры по идентификатору товара в виде guid.
195
+ В тоже время для задач DWH часто нужны данные с менее строгой нормализацией.
196
+ Поэтому тут предложены примеры кода, как сделать инкрементное обновление последующей целевой таблицы с денормализованными данными:
197
+
198
+ [Вариант с сохранением guid ключа в таблице фактов](https://github.com/pavel-v-sobolev/cdc-1c/blob/main/materialize_example.py)
199
+ [Вариант с изменение ключа, использующий GROUP BY, но все равно оптимизированный и инкрементный](https://github.com/pavel-v-sobolev/cdc-1c/blob/main/materialize_example_other_key.py)
200
+
cdc_1c-0.1.0/PLAN.md ADDED
@@ -0,0 +1,124 @@
1
+ # План проекта cdc-1c
2
+
3
+ ## Context
4
+
5
+ Ядро CDC из 1С (OData → БД через dbmerge) реализовано и проверено на живой 1С + PostgreSQL:
6
+ чтение изменений (`ChangeReader1C`), разбор в колоночные `DataObject1C`, транслитерация и лимиты
7
+ имён (`NameMapper1C`), типы/ключи/`delete_key` из метаданных (`MetadataReader1C`), пер-объектное
8
+ сохранение со scoped-delete для регистров/ТЧ (`DBWriter1C`), спец-поля `is_deleted_or_empty` и
9
+ `exchange_message_no`.
10
+
11
+ Текущая цель: оформить наработку как opensource-продукт — Python-пакет на PyPI + Docker-образ для
12
+ запуска «из коробки» с минимумом настроек, и удобный оркестратор с простым вызовом.
13
+
14
+ ## Решения
15
+ - Имя: дистрибутив **cdc-1c**, import-пакет **cdc_1c** (нижний регистр); классы PascalCase.
16
+ - Оркестратор: класс **`Replicator1C`** (суффикс `1C` — как у остальных публичных классов).
17
+ - Конструктор: **отдельные именованные аргументы** (не объект `Config`) — прямой библиотечный
18
+ вызов без обёртки, единый стиль с прочими классами. БД передаётся готовым **`engine`** (DI по-
19
+ sqlalchemy'ному, тестируемость, переиспользование), а не строкой; тот же engine идёт в `DBWriter1C`.
20
+ - `Config` (dataclass) — env-сторона: хранит `db_url`; classmethod **`Replicator1C.from_config(cfg)`**
21
+ строит engine из `db_url` (граница «строка → engine» в одном месте) и маппит поля. `poll_interval`/
22
+ `log_level` остаются в `Config` для `run_forever`/логирования. Entrypoint: `Config.from_env()`.
23
+ - Режимы оркестратора: `run_once()` и `run_forever(interval)`.
24
+ - Python: **>=3.10**. Лицензия: **MIT** (предварительно).
25
+
26
+ ---
27
+
28
+ ## Сделано (ядро CDC)
29
+
30
+ - **`_get_register_records` / удаление наборов** — `_default_key_value`, `_make_deleted_register_record`:
31
+ при пустом `RecordSet` запись дополняется полным ключом из метаданных (дефолты) + реальные
32
+ `Recorder`/`Recorder_Type`.
33
+ - **`NameMapper1C`** — транслитерация; лимит `POSTGRES_MAX_IDENTIFIER = 63` (обрезка + 4-симв. хэш);
34
+ служебные имена `RESERVED_FIELD_NAMES` (`merged_on`, `inserted_on`, `exchange_message_no`): наше
35
+ служебное поле сохраняет имя, поле 1С с таким же транслитом хэшируется; журнал
36
+ `object_mappings`/`field_mappings`. Ручной маппинг убран.
37
+ - **`DataReader1C`** — `DataObject1C.to_records_mapped()` (dict-of-lists → list-of-dict, маппинг
38
+ колонок на лету, без копии); `_convert_value` для `Guid` → `uuid.UUID`.
39
+ - **`DBWriter1C`** — пер-объектное сохранение; удаление по `metadata_obj.delete_key`: документ/
40
+ справочник → `delete_mode='no'`; регистр/ТЧ → `delete_mode='delete'` со scoped `delete_condition`
41
+ (`_scoped_delete_condition`: подзапрос из temp-таблицы — `col IN (SELECT col FROM temp)` или
42
+ row-value `tuple_(...).in_(select(...))`). `Config`/`data_reader` в конструкторе, `save_all()`.
43
+ - **`MetadataReader1C`** — `MetadataObject1C.delete_key` + `_get_delete_key` (регистр →
44
+ `Recorder`/`Recorder_Type`; ТЧ → `Ref_Key`; документ/справочник → None).
45
+ - **Табличные части** — строкам ТЧ проставляется `Ref_Key` владельца (в данных 1С его нет);
46
+ пустая ТЧ → фиктивная запись (`_make_empty_table_part_record`) для scoped-delete по `Ref_Key`.
47
+ - **Спец-поля** — `is_deleted_or_empty` (Boolean: `DeletionMark` документа/справочника, проброс
48
+ пометки в строки ТЧ, `True` у фиктивных записей) и `exchange_message_no` (номер пакета обмена,
49
+ во всех записях; ставится в `ChangeReader.read_changes`).
50
+ - **A1. Переименование** — `src/cdc_1C` → `src/cdc_1c`, все импорты `cdc_1C` → `cdc_1c`,
51
+ `pyproject` `name = "cdc-1c"`, `version("cdc-1c")`. Пересобрано `uv sync` (cdc-1c==0.1.0).
52
+
53
+ ---
54
+
55
+ ## Осталось
56
+
57
+ ### Фаза A. Hardening ядра
58
+ - **A2. Конфигурируемый auth** — ✅ (частично) `MetadataReader1C`/`DataReader1C.__init__` принимают
59
+ `auth: tuple[str,str] | None`, хранят `self.auth`; все `requests.*` используют `auth=self.auth`;
60
+ `ChangeReader1C` пробрасывает `auth`. Захардкоженные `('admin','admin')` убраны (4 места).
61
+ Осталось: общий `requests.Session`, `timeout`, опц. `verify`.
62
+ - **A3. Сброс состояния** — ✅ `ChangeReader1C.read_changes()` в начале делает `self.clear()`.
63
+ - **A4. `Config` (dataclass)** — ✅ (частично) `src/cdc_1c/config.py`: `odata_url`, `odata_user`,
64
+ `odata_password`, `exchange_name`, `queue_guid`, `db_url`, `db_schema`, `poll_interval`, `log_level`;
65
+ `from_env()` читает `CDC1C_*`. Осталось: опц. `request_timeout`, `verify_ssl` (вместе с остатком A2).
66
+ - **A5. Оркестратор `Replicator1C`** — ✅ `src/cdc_1c/replicator.py`: собирает
67
+ metadata/changes/mapper/writer из отдельных аргументов (engine + auth из user/password); classmethod
68
+ `from_config(Config)` строит engine из `db_url`; `run_once()` (read → save → **notify только после
69
+ успешного save**); `run_forever(interval)` с обработкой исключений (упал цикл → лог, без notify,
70
+ повтор) и graceful SIGTERM/SIGINT (`_StopSignal`). Экспортирован из `cdc_1c`.
71
+ - **A6. Логирование** — ✅ во всех модулях `logger = logging.getLogger(__name__)`, `basicConfig()`/root
72
+ убраны (ридеры/writer/replicator/config). Авто-вывод из коробки — `src/cdc_1c/logging_config.py`
73
+ `_ensure_handler()` (вешает StreamHandler на логгер `cdc_1c` с INFO, только если `hasHandlers()` ==
74
+ False), вызывается из `Replicator1C.__init__`. Если приложение настроило логирование — молчим.
75
+ Уровень из `Config.log_level` — настраивать в entrypoint (B2). Проверено: на чистом root INFO виден
76
+ из коробки, при настроенном приложением логировании — молчим. Зависимость **dbmerge** переведена на
77
+ тот же паттерн (`getLogger('dbmerge')` + `hasHandlers()`, без `basicConfig`) — root больше не
78
+ загрязняется, обе библиотеки сосуществуют чисто.
79
+ - ✅ Файлы модулей в snake_case (`metadata_reader.py`, `data_reader.py`, `change_reader.py`,
80
+ `name_mapper.py`, `db_writer.py`); классы и реэкспорт из `__init__` не изменены, публичный API
81
+ (`from cdc_1c import …`) прежний.
82
+
83
+ ### Фаза B. Упаковка для PyPI
84
+ - **B1. `pyproject.toml`** — описание, `license = "MIT"`, авторы, `requires-python = ">=3.10"`,
85
+ keywords/classifiers/urls; ядро: `dbmerge`, `requests`, `sqlalchemy`, `xmltodict` (убрать `polars`
86
+ и `psycopg2` из обязательных); `[project.optional-dependencies] postgres = ["psycopg[binary]>=3.2"]`
87
+ (psycopg3, DSN `postgresql+psycopg`); `[project.scripts] cdc-1c = "cdc_1c.__main__:main"`.
88
+ - **B2. Entrypoint** — `src/cdc_1c/__main__.py`: `Config.from_env()` → `Replicator`; режим
89
+ `CDC1C_MODE=once|loop` (по умолчанию loop). Работает как `python -m cdc_1c` и команда `cdc-1c`.
90
+ - **B3.** `LICENSE`, `src/cdc_1c/py.typed`, `CHANGELOG.md`; английский README (quickstart pip+Docker,
91
+ таблица ENV, требования к плану обмена OData в 1С, поведение имён, спец-поля, ограничения);
92
+ `main.py` → краткий пример использования `Replicator` (или удалить).
93
+
94
+ ### Фаза C. Docker «из коробки»
95
+ - **C1. `Dockerfile`** — `python:3.12-slim`, `cdc-1c[postgres]`, `ENTRYPOINT ["cdc-1c"]`, ENV,
96
+ graceful SIGTERM.
97
+ - **C2. `docker-compose.yml`** — сервис `cdc-1c` + опциональный `postgres`; `.env.example`;
98
+ README-раздел «Запуск в Docker за 1 минуту».
99
+
100
+ ### Фаза D. Качество
101
+ - **D1. Тесты (pytest)** — `NameMapper1C`, `to_records_mapped`, `_get_delete_key`; на sqlite —
102
+ `DBWriter1C` scoped-delete (регистр/ТЧ/пустая ТЧ), идемпотентность, фиктивные записи,
103
+ `is_deleted_or_empty` по всем веткам, GUID→`uuid.UUID`; ридеры — фикстуры реального XML
104
+ `SelectChanges`/`$metadata`.
105
+ - **D2. CI (GitHub Actions)** — ruff, тесты на матрице 3.10–3.13, сборка; публикация на PyPI по
106
+ git-тегу (OIDC/trusted publishing).
107
+
108
+ ---
109
+
110
+ ## Известные ограничения (→ README)
111
+ - Фиктивные записи (удалённый набор регистра, пустая ТЧ) вставляются как заглушка с дефолтным
112
+ ключом (`LineNumber=0`); реальные старые строки удаляются scoped-delete'ом. Отличаются по
113
+ `is_deleted_or_empty=True`.
114
+ - Опустевшая ТЧ как `xsi:nil` сейчас отфильтровывается (`_get_record_table_parts`) — уточнить формат
115
+ 1С и допокрыть при необходимости.
116
+ - Уникальность обрезанных длинных имён строго не гарантируется (хэш от полного имени).
117
+
118
+ ## Проверка (end-to-end)
119
+ 1. `uv sync`; `pytest` — зелёные offline-тесты (без 1С/Postgres).
120
+ 2. `Config(...)` → `Replicator(cfg).run_once()` против живой 1С + Postgres: таблицы, типы,
121
+ scoped-delete, спец-поля; повторный прогон — идемпотентность.
122
+ 3. `run_forever(interval=…)`: цикл чистит состояние, notify только после успешного save, реакция на SIGTERM.
123
+ 4. Docker: `docker compose up` с заполненным `.env` → данные грузятся без правок кода; `docker stop` штатно завершает.
124
+ 5. `uv build` + установка из wheel в чистом окружении (3.10): импорт и `cdc-1c`/`python -m cdc_1c`.