simple-db-settings 1.0.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 (36) hide show
  1. simple_db_settings-1.0.0/.github/workflows/publish.yml +36 -0
  2. simple_db_settings-1.0.0/.gitignore +13 -0
  3. simple_db_settings-1.0.0/CHANGELOG.md +5 -0
  4. simple_db_settings-1.0.0/LICENSE +21 -0
  5. simple_db_settings-1.0.0/PKG-INFO +385 -0
  6. simple_db_settings-1.0.0/README.md +348 -0
  7. simple_db_settings-1.0.0/art/banner.svg +146 -0
  8. simple_db_settings-1.0.0/art/flow.svg +155 -0
  9. simple_db_settings-1.0.0/compose.tests.yml +27 -0
  10. simple_db_settings-1.0.0/pyproject.toml +56 -0
  11. simple_db_settings-1.0.0/src/simple_db_settings/__init__.py +41 -0
  12. simple_db_settings-1.0.0/src/simple_db_settings/audit.py +75 -0
  13. simple_db_settings-1.0.0/src/simple_db_settings/cache.py +70 -0
  14. simple_db_settings-1.0.0/src/simple_db_settings/cli.py +183 -0
  15. simple_db_settings-1.0.0/src/simple_db_settings/codec.py +67 -0
  16. simple_db_settings-1.0.0/src/simple_db_settings/exceptions.py +38 -0
  17. simple_db_settings-1.0.0/src/simple_db_settings/py.typed +0 -0
  18. simple_db_settings-1.0.0/src/simple_db_settings/schema.py +50 -0
  19. simple_db_settings-1.0.0/src/simple_db_settings/store.py +306 -0
  20. simple_db_settings-1.0.0/src/simple_db_settings/transfer.py +87 -0
  21. simple_db_settings-1.0.0/src/simple_db_settings/typed.py +86 -0
  22. simple_db_settings-1.0.0/src/simple_db_settings/upsert.py +42 -0
  23. simple_db_settings-1.0.0/tests/conftest.py +16 -0
  24. simple_db_settings-1.0.0/tests/fixtures/php_rows.json +95 -0
  25. simple_db_settings-1.0.0/tests/integration/test_dialects.py +93 -0
  26. simple_db_settings-1.0.0/tests/test_audit.py +110 -0
  27. simple_db_settings-1.0.0/tests/test_cache.py +124 -0
  28. simple_db_settings-1.0.0/tests/test_cli.py +130 -0
  29. simple_db_settings-1.0.0/tests/test_codec.py +97 -0
  30. simple_db_settings-1.0.0/tests/test_package.py +27 -0
  31. simple_db_settings-1.0.0/tests/test_parity.py +79 -0
  32. simple_db_settings-1.0.0/tests/test_php_compat.py +35 -0
  33. simple_db_settings-1.0.0/tests/test_schema.py +56 -0
  34. simple_db_settings-1.0.0/tests/test_store.py +153 -0
  35. simple_db_settings-1.0.0/tests/test_transfer.py +128 -0
  36. simple_db_settings-1.0.0/tests/test_typed.py +114 -0
@@ -0,0 +1,36 @@
1
+ name: publish
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: python -m pip install build
17
+ - run: python -m build
18
+ - uses: actions/upload-artifact@v4
19
+ with:
20
+ name: dist
21
+ path: dist/
22
+
23
+ publish:
24
+ needs: build
25
+ runs-on: ubuntu-latest
26
+ environment:
27
+ name: pypi
28
+ url: https://pypi.org/p/simple-db-settings
29
+ permissions:
30
+ id-token: write
31
+ steps:
32
+ - uses: actions/download-artifact@v4
33
+ with:
34
+ name: dist
35
+ path: dist/
36
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .coverage
5
+ cov_annotate/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .venv/
10
+ .env
11
+ .DS_Store
12
+ .idea/
13
+ .vscode/
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 1.0.0
4
+
5
+ Первый релиз: store (mapping protocol), кодек json + чтение PHP-типов, TTL-кеш, аудит, export/import, typed-слой (pydantic), CLI (typer).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timur Turdyev
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.
@@ -0,0 +1,385 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple-db-settings
3
+ Version: 1.0.0
4
+ Summary: DB-backed settings manager on SQLAlchemy, table-compatible with timurturdyev/simple-settings
5
+ Project-URL: Homepage, https://github.com/TimurTurdyev/simple-settings-py
6
+ Project-URL: Repository, https://github.com/TimurTurdyev/simple-settings-py
7
+ Project-URL: Issues, https://github.com/TimurTurdyev/simple-settings-py/issues
8
+ Project-URL: Changelog, https://github.com/TimurTurdyev/simple-settings-py/blob/main/CHANGELOG.md
9
+ Author: Timur Turdyev
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: sqlalchemy>=2.0
23
+ Provides-Extra: cli
24
+ Requires-Dist: typer>=0.12; extra == 'cli'
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
27
+ Requires-Dist: pytest>=8.0; extra == 'dev'
28
+ Provides-Extra: pydantic
29
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
30
+ Provides-Extra: redis
31
+ Requires-Dist: redis>=5.0; extra == 'redis'
32
+ Provides-Extra: test-db
33
+ Requires-Dist: cryptography>=42.0; extra == 'test-db'
34
+ Requires-Dist: psycopg[binary]>=3.1; extra == 'test-db'
35
+ Requires-Dist: pymysql>=1.1; extra == 'test-db'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # Simple DB Settings
39
+
40
+ <p align="center">
41
+ <img src="art/banner.svg" alt="Simple DB Settings" width="100%">
42
+ </p>
43
+
44
+ Менеджер настроек в БД для Python на SQLAlchemy 2: группы, TTL-кэш, аудит изменений, типизация через pydantic. Идея взята из Laravel-пакета [timurturdyev/simple-settings](https://github.com/timurturdyev/simple-settings) и совместима с ним по таблице: приложения на Python и PHP могут работать с одними и теми же настройками.
45
+
46
+ [English](#english)
47
+
48
+ ---
49
+
50
+ ## Требования
51
+
52
+ - Python 3.11+
53
+ - SQLAlchemy 2.0+
54
+ - Любая СУБД с драйвером для SQLAlchemy: SQLite, PostgreSQL, MySQL/MariaDB
55
+
56
+ ## Установка
57
+
58
+ ```bash
59
+ pip install simple-db-settings
60
+ ```
61
+
62
+ Дополнительные возможности ставятся экстрами:
63
+
64
+ ```bash
65
+ pip install "simple-db-settings[pydantic]" # типизированные группы
66
+ pip install "simple-db-settings[cli]" # консольная команда
67
+ ```
68
+
69
+ ## Быстрый старт
70
+
71
+ ```python
72
+ from sqlalchemy import create_engine
73
+ from simple_db_settings import SettingsStore
74
+
75
+ engine = create_engine("sqlite:///app.db")
76
+ store = SettingsStore(engine)
77
+ store.create_tables() # или создайте таблицы своей миграцией
78
+
79
+ site = store.group("site")
80
+ site["name"] = "My App"
81
+ site["per_page"] = 15
82
+
83
+ site["name"] # 'My App'
84
+ site.get("missing", "default") # 'default'
85
+ ```
86
+
87
+ ## Группы
88
+
89
+ Настройки разделены по группам. Группа по умолчанию - `global`.
90
+
91
+ ```python
92
+ email = store.group("email")
93
+ email["host"] = "smtp.example.com"
94
+
95
+ store.group("site").get("host") # None - другая группа
96
+ store.groups() # ['email', 'site']
97
+ ```
98
+
99
+ `GroupView` ведет себя как обычный словарь, работают все привычные операции:
100
+
101
+ ```python
102
+ "host" in email # True
103
+ len(email) # 1
104
+ dict(email) # {'host': 'smtp.example.com'}
105
+ del email["host"] # удалить ключ
106
+ email.clear() # удалить все настройки группы
107
+ ```
108
+
109
+ ## Типы данных
110
+
111
+ Значения сериализуются в JSON и восстанавливаются без ручного приведения:
112
+
113
+ ```python
114
+ site["count"] = 42 # int
115
+ site["price"] = 9.99 # float
116
+ site["enabled"] = True # bool
117
+ site["tags"] = ["a", "b"] # list
118
+ site["meta"] = {"k": "v"} # dict
119
+ site["empty"] = None # None
120
+ ```
121
+
122
+ ## Массовая запись
123
+
124
+ ```python
125
+ site.update({
126
+ "name": "My App",
127
+ "url": "https://example.com",
128
+ "per_page": 15,
129
+ })
130
+ ```
131
+
132
+ Вся пачка уходит в БД одним upsert-запросом в одной транзакции. Валидация идет до записи: невалиден хоть один ключ - не сохранится ни один. Кэш сбрасывается один раз после коммита.
133
+
134
+ ## Кэш
135
+
136
+ Чтение идет через TTL-кэш (по умолчанию 5 секунд), кэшируется карта группы целиком.
137
+
138
+ ```python
139
+ from simple_db_settings import NullCache
140
+
141
+ store = SettingsStore(engine, cache_ttl=30) # свой TTL
142
+ store = SettingsStore(engine, cache=NullCache()) # без кэша
143
+
144
+ site.fresh() # прочитать группу из БД мимо кэша
145
+ ```
146
+
147
+ Кэш инвалидируется только после коммита транзакции, поэтому параллельный запрос не затянет в кэш еще не зафиксированные данные. При нескольких процессах (gunicorn, celery) устаревание ограничено TTL.
148
+
149
+ Свой бэкенд - любой объект с методами `get` / `set` / `invalidate` (протокол `CacheBackend`), например обертка над Redis.
150
+
151
+ ## Транзакции
152
+
153
+ По умолчанию каждая операция - отдельная короткая транзакция. Чтобы записать настройки атомарно вместе со своими данными, передайте соединение:
154
+
155
+ ```python
156
+ with engine.begin() as conn:
157
+ conn.execute(orders_table.insert().values(user_id=1))
158
+ store.with_connection(conn).group("site")["last_order"] = 1
159
+ ```
160
+
161
+ `with_connection()` возвращает копию стора на внешнем соединении: она ничего не коммитит сама, коммит общий, кэш сбросится после него.
162
+
163
+ ## Аудит
164
+
165
+ Опциональная история изменений в таблице `simple_setting_changes`:
166
+
167
+ ```python
168
+ from simple_db_settings import SettingsStore, causer
169
+
170
+ store = SettingsStore(engine, audit=True)
171
+
172
+ with causer("user", 42):
173
+ store.group("site")["per_page"] = 20
174
+ ```
175
+
176
+ Каждая запись и удаление кладет строку: `event` (`created` / `updated` / `deleted`), `old_payload`, `new_payload`, `causer_type`, `causer_id`, `created_at`. Строки аудита пишутся в той же транзакции, что и сами настройки: либо сохранилось все, либо ничего. Вне контекста `causer()` автор будет `NULL`.
177
+
178
+ Как и в PHP-пакете, `clear()` записей в истории не создает, а холостое удаление (ключа нет) не оставляет следов.
179
+
180
+ Чтение истории - обычный select:
181
+
182
+ ```python
183
+ from sqlalchemy import MetaData, select
184
+ from simple_db_settings.schema import make_changes_table
185
+
186
+ changes = make_changes_table(MetaData())
187
+ with engine.connect() as conn:
188
+ rows = conn.execute(
189
+ select(changes)
190
+ .where(changes.c.group == "site", changes.c.name == "per_page")
191
+ .order_by(changes.c.created_at.desc())
192
+ .limit(20)
193
+ ).all()
194
+ ```
195
+
196
+ ## Типизированные настройки
197
+
198
+ Экстра `pydantic`. Дефолты живут в коде, БД хранит только отличия от них:
199
+
200
+ ```python
201
+ from pydantic import BaseModel
202
+
203
+ class SiteSettings(BaseModel):
204
+ name: str = "My App"
205
+ per_page: int = 15
206
+ maintenance: bool = False
207
+
208
+ typed = store.typed(SiteSettings, group="site")
209
+
210
+ cfg = typed.load() # дефолты + оверрайды из БД
211
+ cfg.per_page = 50
212
+ typed.save(cfg) # в БД уйдет только per_page
213
+
214
+ typed.reset("per_page") # вернуть поле к дефолту
215
+ typed.reset() # вернуть все поля
216
+ ```
217
+
218
+ `save()` удаляет из БД поля, значение которых совпало с дефолтом, поэтому смена дефолта в коде сразу видна везде, где поле не переопределяли.
219
+
220
+ ## Консольная команда
221
+
222
+ Экстра `cli`. URL базы передается флагом `--url` или переменной окружения `SIMPLE_DB_SETTINGS_URL`.
223
+
224
+ ```bash
225
+ export SIMPLE_DB_SETTINGS_URL="sqlite:///app.db"
226
+
227
+ # Получить и установить (значение парсится как JSON, иначе строка)
228
+ simple-db-settings get site_name
229
+ simple-db-settings get host --group email
230
+ simple-db-settings set per_page 15
231
+ simple-db-settings set tags '["a","b"]'
232
+
233
+ # Список настроек и групп
234
+ simple-db-settings list
235
+ simple-db-settings list --group email
236
+ simple-db-settings groups
237
+
238
+ # Удаление
239
+ simple-db-settings delete site_name
240
+ simple-db-settings clear --group email
241
+
242
+ # Экспорт и импорт JSON
243
+ simple-db-settings export backup.json
244
+ simple-db-settings export --group email
245
+ simple-db-settings import backup.json
246
+ simple-db-settings import backup.json --replace
247
+ ```
248
+
249
+ ## Экспорт и импорт
250
+
251
+ ```python
252
+ from simple_db_settings import export_json, import_json
253
+
254
+ dump = export_json(store) # все группы
255
+ dump = export_json(store, "email") # одна группа
256
+
257
+ import_json(store, dump) # merge: существующие ключи перезаписываются
258
+ import_json(store, dump, replace=True) # сначала очистить группы из файла
259
+ ```
260
+
261
+ Файл несет сырые `val` и `type`, поэтому типы переносятся без потерь. Формат совместим с `setting:export` / `setting:import` из PHP-пакета в обе стороны. Валидация идет до записи: битая запись в файле - и не импортируется ничего.
262
+
263
+ ## Совместимость с Laravel-пакетом
264
+
265
+ Пакет работает с той же таблицей, что и timurturdyev/simple-settings v6:
266
+
267
+ - схема идентична: `(group, name, val, type, created_at, updated_at)`, составной первичный ключ `(group, name)`
268
+ - Python читает все типы, которые пишет PHP: `string`, `integer`, `float`, `boolean`, `array`, `object`, `null` (и legacy `double`)
269
+ - Python пишет `type = 'json'`; PHP читает его начиная с v6.1
270
+ - таблица аудита и формат export/import тоже общие
271
+
272
+ Одна таблица настроек - разные приложения на разных языках.
273
+
274
+ ## Как это работает
275
+
276
+ <p align="center">
277
+ <img src="art/flow.svg" alt="Схема взаимодействия" width="100%">
278
+ </p>
279
+
280
+ **Точка входа** - `SettingsStore`: ему отдают engine и, по желанию, имя таблицы, кэш и флаг аудита. Стор сам ничего не хранит, он раздает `GroupView` - живые представления групп, через которые идет вся работа. CLI, типизированный слой и export/import - обертки над теми же `GroupView`, отдельных путей к БД у них нет.
281
+
282
+ **Чтение.** `site["per_page"]` сначала смотрит в кэш. Если карта группы там и не протухла - БД не трогается вообще. При промахе одним select-ом читается вся группа, каждая строка прогоняется через кодек (по колонке `type`) и готовая карта кладется в кэш. Следующие чтения любой настройки этой группы бесплатны до истечения TTL.
283
+
284
+ **Запись.** `site["x"] = 1` и `site.update({...})` собирают пачку, кодируют значения в JSON и отправляют одним upsert-запросом: insert с обработкой конфликта по `(group, name)` на диалекте вашей СУБД. Если включен аудит, в той же транзакции читаются старые значения и одним insert-ом пишутся строки истории. Кэш группы сбрасывается строго после коммита.
285
+
286
+ **Внешняя транзакция.** Стор, полученный через `with_connection()`, выполняет те же операции на вашем соединении и не коммитит: судьбу транзакции решает вызывающий код. Сброс кэша откладывается до реального коммита, а откат не оставляет в кэше мусора.
287
+
288
+ **Вторая сторона таблицы.** Laravel-приложение с пакетом simple-settings ходит в ту же таблицу со своим кэшем. Оба пакета переживают типы через колонку `type`, поэтому настройка, записанная одним, корректно читается другим.
289
+
290
+ ## Лимиты значений
291
+
292
+ Колонка `val` создается как `TEXT` - на MySQL/MariaDB это 65 535 байт (~64 KB), на PostgreSQL и SQLite ограничения нет. Для типовых настроек этого с большим запасом. Если уперлись в лимит - это сигнал, что в одну настройку положили что-то не то (каталог товаров, лог, контент). Таким данным нужна своя таблица.
293
+
294
+ ## Схема БД
295
+
296
+ ```
297
+ simple_settings
298
+ group string
299
+ name string
300
+ val text
301
+ type char(20)
302
+ created_at
303
+ updated_at
304
+
305
+ PRIMARY KEY (group, name)
306
+
307
+ simple_setting_changes
308
+ id bigint PK
309
+ group string
310
+ name string
311
+ event string created / updated / deleted
312
+ old_payload text NULL
313
+ new_payload text NULL
314
+ causer_type string NULL
315
+ causer_id bigint NULL
316
+ created_at
317
+ ```
318
+
319
+ ## Справочник API
320
+
321
+ | Метод | Описание |
322
+ |-------|----------|
323
+ | `SettingsStore(engine, table_name=..., cache=..., cache_ttl=..., audit=..., changes_table_name=...)` | Создать стор |
324
+ | `store.group(name)` | `GroupView` для группы |
325
+ | `store.groups()` | Список всех групп |
326
+ | `store.rows(group=None)` | Сырые строки с фильтром по группе |
327
+ | `store.typed(ModelCls, group=...)` | Типизированная группа (pydantic) |
328
+ | `store.with_connection(conn)` | Копия стора на внешнем соединении |
329
+ | `store.create_tables()` | Создать таблицы |
330
+ | `view[key]` / `view.get(key, default)` | Получить значение |
331
+ | `view[key] = value` | Записать значение |
332
+ | `del view[key]` | Удалить ключ |
333
+ | `view.update(mapping)` | Записать пачку атомарно |
334
+ | `view.fresh()` | Прочитать группу мимо кэша |
335
+ | `view.clear()` | Удалить все настройки группы |
336
+ | `causer(type, id)` | Контекст автора изменений для аудита |
337
+ | `export_json(store, group=None)` | Экспорт в JSON-строку |
338
+ | `import_json(store, text, group=None, replace=False)` | Импорт из JSON-строки |
339
+
340
+ ## Лицензия
341
+
342
+ MIT
343
+
344
+ ---
345
+
346
+ ## English
347
+
348
+ DB-backed settings manager for Python on SQLAlchemy 2: group namespacing, TTL cache, change audit, typed groups via pydantic. Table-compatible with the Laravel package [timurturdyev/simple-settings](https://github.com/timurturdyev/simple-settings): Python and PHP apps can share the same settings table.
349
+
350
+ **Requirements:** Python 3.11+, SQLAlchemy 2.0+; SQLite, PostgreSQL or MySQL/MariaDB.
351
+
352
+ **Install:**
353
+
354
+ ```bash
355
+ pip install simple-db-settings
356
+ pip install "simple-db-settings[pydantic,cli]" # optional extras
357
+ ```
358
+
359
+ **Basic usage:**
360
+
361
+ ```python
362
+ from sqlalchemy import create_engine
363
+ from simple_db_settings import SettingsStore
364
+
365
+ store = SettingsStore(create_engine("sqlite:///app.db"))
366
+ store.create_tables()
367
+
368
+ site = store.group("site")
369
+ site["per_page"] = 15
370
+ site.get("missing", "default")
371
+ site.update({"a": 1, "b": 2}) # single upsert, all-or-nothing
372
+ site.fresh() # bypass cache
373
+ ```
374
+
375
+ Values are stored as JSON and restored automatically (int, float, bool, str, list, dict, None). Reads go through a per-group TTL cache invalidated only after commit. Pass a connection via `store.with_connection(conn)` to join your own transaction.
376
+
377
+ **Audit:** `SettingsStore(engine, audit=True)` records every create/update/delete into `simple_setting_changes` within the same transaction; wrap calls in `causer("user", 42)` to attach the author.
378
+
379
+ **Typed groups:** defaults live in code, the DB keeps only overrides; `typed.save()` deletes rows that returned to their defaults.
380
+
381
+ **CLI:** `simple-db-settings get/set/delete/list/groups/clear/export/import` with `--url` or `SIMPLE_DB_SETTINGS_URL`.
382
+
383
+ **Cross-language:** same table schema as the Laravel package v6; Python reads every PHP type and writes `type = 'json'`, which PHP reads since v6.1. Export files are interchangeable.
384
+
385
+ For full documentation see the Russian section above.