sqlmodel-object-helpers 0.0.6__tar.gz → 0.0.8__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 (50) hide show
  1. sqlmodel_object_helpers-0.0.8/.git-blame-ignore-revs +5 -0
  2. sqlmodel_object_helpers-0.0.8/.gitattributes +16 -0
  3. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/.gitignore +11 -1
  4. sqlmodel_object_helpers-0.0.8/.vscode/settings.json +81 -0
  5. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/PKG-INFO +30 -9
  6. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/README.md +27 -3
  7. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/pyproject.toml +11 -11
  8. sqlmodel_object_helpers-0.0.8/pyrightconfig.json +38 -0
  9. sqlmodel_object_helpers-0.0.8/ruff.toml +129 -0
  10. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/__init__.py +29 -28
  11. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/constants.py +42 -42
  12. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/dynamic_meta.py +18 -28
  13. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/filters.py +57 -29
  14. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/loaders.py +3 -1
  15. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/mutations.py +508 -498
  16. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/operators.py +7 -0
  17. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/query.py +304 -138
  18. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/session.py +162 -153
  19. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/standalone.py +193 -19
  20. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/datetime.py +3 -6
  21. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/filters.py +37 -13
  22. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/conftest.py +810 -750
  23. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_bulk_mutations.py +49 -14
  24. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_column_meta.py +27 -24
  25. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_computed_columns.py +14 -11
  26. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_count_exists.py +45 -18
  27. sqlmodel_object_helpers-0.0.8/tests/test_data_error_handling.py +152 -0
  28. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_datetime_range.py +37 -18
  29. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_dynamic_meta.py +113 -65
  30. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_exceptions.py +195 -196
  31. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_filters.py +339 -364
  32. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_for_update.py +5 -3
  33. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_generated_columns_pg.py +15 -9
  34. sqlmodel_object_helpers-0.0.8/tests/test_implicit_datetime.py +182 -0
  35. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_loaders.py +209 -204
  36. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_mutations.py +418 -386
  37. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_operators.py +191 -192
  38. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_query.py +653 -569
  39. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_settings.py +164 -165
  40. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_standalone.py +195 -86
  41. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_time_filter.py +48 -41
  42. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/tests/test_types.py +859 -844
  43. sqlmodel_object_helpers-0.0.8/uv.lock +557 -0
  44. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/.github/workflows/publish.yml +0 -0
  45. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/LICENSE +0 -0
  46. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/exceptions.py +0 -0
  47. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/__init__.py +0 -0
  48. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/columns.py +0 -0
  49. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/pagination.py +0 -0
  50. {sqlmodel_object_helpers-0.0.6 → sqlmodel_object_helpers-0.0.8}/src/sqlmodel_object_helpers/types/projections.py +0 -0
@@ -0,0 +1,5 @@
1
+ # Коммиты, которые меняют только форматирование. Включить локально:
2
+ # git config blame.ignoreRevsFile .git-blame-ignore-revs
3
+
4
+ # Нормализация переводов строк в LF, 2026-09-08
5
+ 118793bd5af93982889c382fb19b6c55aedc5dd8
@@ -0,0 +1,16 @@
1
+ # Enforce LF line endings for all text files
2
+ * text=auto eol=lf
3
+
4
+ # Explicitly mark as text
5
+ *.py text eol=lf
6
+ *.toml text eol=lf
7
+ *.md text eol=lf
8
+ *.txt text eol=lf
9
+ *.json text eol=lf
10
+ *.yaml text eol=lf
11
+ *.yml text eol=lf
12
+
13
+ # Binary files (no conversion)
14
+ *.png binary
15
+ *.jpg binary
16
+ *.ico binary
@@ -21,7 +21,8 @@ htmlcov/
21
21
 
22
22
  # IDEs
23
23
  .idea/
24
- .vscode/
24
+ .vscode/*
25
+ !.vscode/settings.json
25
26
  *.swp
26
27
  *.swo
27
28
 
@@ -32,3 +33,12 @@ htmlcov/
32
33
  Thumbs.db
33
34
  Desktop.ini
34
35
  .DS_Store
36
+
37
+ # --- личное окружение разработчика (общий блок ci-templates) ---
38
+ .claude/
39
+ .cursor/
40
+ .windsurf/
41
+
42
+ # --- кеши инструментов ---
43
+ .uv-cache/
44
+ .ruff_cache/
@@ -0,0 +1,81 @@
1
+ {
2
+ // Общий стандарт python-парка. Настройки редактора.
3
+ // Источник: devops/ci-templates/standards/vscode-settings.json @ v1.18.0
4
+ // Кладётся в репозиторий как .vscode/settings.json.
5
+ //
6
+ // ЛИЧНОЕ СЮДА НЕ ДОПИСЫВАТЬ. Файл общий, его sha256 сверяет блокирующая
7
+ // джоба standards_check. Привычки редактора (autofetch, renderWhitespace,
8
+ // suggest.*, тема) и настройки своих расширений держите в СВОИХ настройках
9
+ // VS Code: Ctrl+Shift+P -> Preferences: Open User Settings (JSON). Оттуда
10
+ // они работают сразу во всех репозиториях парка, а не в одном, и никакой
11
+ // pull их не тронет. Всё прочее в .vscode/ (mcp.json, sftp.json) в git
12
+ // не едет и никогда не ехало.
13
+
14
+ // Главное. Без fromEnvironment расширение берёт СВОЙ встроенный ruff
15
+ // произвольной версии, и результат у разработчика расходится с CI даже при
16
+ // одинаковом конфиге. С этой настройкой берётся ruff из .venv проекта,
17
+ // то есть ровно тот, что запинен в uv.lock и запускается в пайплайне.
18
+ "ruff.importStrategy": "fromEnvironment",
19
+
20
+ // Второе главное. Умолчание расширения — editorFirst: ключи ruff.lint.select,
21
+ // ruff.lint.ignore и ruff.lineLength из настроек редактора ПЕРЕБИВАЮТ
22
+ // ruff.toml. standards_check при этом зелёный — он сверяет сам ruff.toml, —
23
+ // а разработчик локально линтует другим набором правил и форматирует по
24
+ // другой ширине. filesystemFirst возвращает единственный источник правды.
25
+ "ruff.configurationPreference": "filesystemFirst",
26
+
27
+ "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
28
+
29
+ "[python]": {
30
+ "editor.defaultFormatter": "charliermarsh.ruff",
31
+ "editor.formatOnSave": true,
32
+ "editor.codeActionsOnSave": {
33
+ "source.fixAll.ruff": "explicit",
34
+ "source.organizeImports.ruff": "explicit"
35
+ },
36
+ // Линейка совпадает с line-length в ruff.toml.
37
+ "editor.rulers": [120]
38
+ },
39
+
40
+ // Проверку типов ведёт pyright по общему pyrightconfig.json,
41
+ // поэтому встроенный анализатор не дублирует его своим режимом.
42
+ "python.analysis.typeCheckingMode": "off",
43
+ "python.analysis.autoImportCompletions": true,
44
+
45
+ // Файлы общего стандарта редактор держит только на чтение. Закрывает частый
46
+ // случай: при "[json]": {"editor.formatOnSave": true} открытие и сохранение
47
+ // pyrightconfig.json переформатирует его и уронит standards_check.
48
+ // Разовая правка — команда «File: Toggle Active Editor Read-only in Session».
49
+ "files.readonlyInclude": {
50
+ "ruff.toml": true,
51
+ "pyrightconfig.json": true,
52
+ ".vscode/settings.json": true
53
+ },
54
+
55
+ // Команда работает и на Windows, и на Linux, а один случайный CRLF даёт
56
+ // diff на весь файл. Со стороны git то же самое делает .gitattributes.
57
+ "files.eol": "\n",
58
+ "files.trimTrailingWhitespace": true,
59
+ "files.insertFinalNewline": true,
60
+
61
+ // Вотчер .gitignore не читает вообще, исключения ему нужны свои. У
62
+ // sro-agent-core [tool.uv] cache-dir = ".uv-cache" держит кеш пакетов
63
+ // ВНУТРИ репозитория — без этого редактор ходит по распакованным колёсам.
64
+ "files.watcherExclude": {
65
+ "**/__pycache__": true,
66
+ "**/.venv": true,
67
+ "**/.uv-cache": true,
68
+ "**/.ruff_cache": true,
69
+ "**/.pytest_cache": true
70
+ },
71
+
72
+ // Поиск по умолчанию уважает .gitignore, но тот дырявый: .uv-cache закрыт
73
+ // в 3 репозиториях из 19, .pytest_cache — в 11.
74
+ "search.exclude": {
75
+ "**/__pycache__": true,
76
+ "**/.venv": true,
77
+ "**/.uv-cache": true,
78
+ "**/.ruff_cache": true,
79
+ "**/.pytest_cache": true
80
+ }
81
+ }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: sqlmodel-object-helpers
3
- Version: 0.0.6
3
+ Version: 0.0.8
4
4
  Summary: Generic async query helpers for SQLModel: filtering, eager loading, pagination
5
5
  Project-URL: Homepage, https://github.com/itstandart/sqlmodel-object-helpers
6
6
  Project-URL: Repository, https://github.com/itstandart/sqlmodel-object-helpers
@@ -22,13 +22,10 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Typing :: Typed
23
23
  Requires-Python: >=3.14
24
24
  Requires-Dist: pydantic>=2.12
25
- Requires-Dist: sqlalchemy>=2.0.46
25
+ Requires-Dist: sqlalchemy[asyncio]>=2.0.46
26
26
  Requires-Dist: sqlmodel>=0.0.22
27
27
  Provides-Extra: dev
28
- Requires-Dist: aiosqlite>=0.20; extra == 'dev'
29
28
  Requires-Dist: mypy>=1.13.0; extra == 'dev'
30
- Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
31
- Requires-Dist: pytest>=7.0; extra == 'dev'
32
29
  Requires-Dist: ruff>=0.8.0; extra == 'dev'
33
30
  Description-Content-Type: text/markdown
34
31
 
@@ -295,7 +292,7 @@ result = await soh.get_objects(
295
292
  users = await soh.get_objects(
296
293
  session, User,
297
294
  filters={"is_active": {soh.Operator.EQ: True}, "role": {soh.Operator.EQ: "admin"}},
298
- logical_operator="OR",
295
+ logical_operator=soh.LogicalOperator.OR,
299
296
  )
300
297
 
301
298
  # Relationship filters (dot-notation)
@@ -523,6 +520,8 @@ Used by `get_objects`. Nested dicts are auto-flattened via `flatten_filters`:
523
520
 
524
521
  `Operator` is a `StrEnum` — each member equals its string value (`Operator.EQ == "eq"`), so string keys still work but enum members provide type safety and autocompletion.
525
522
 
523
+ `logical_operator` of `get_objects`, `count_objects`, `update_objects` and `delete_objects` takes `soh.LogicalOperator.AND` (default) or `soh.LogicalOperator.OR`; the strings `"AND"`/`"OR"` in any case still work. Any other value raises `InvalidFilterError`, even with `suspend_error=True`.
524
+
526
525
  Multiple operators can be combined on a single field: `{"age": {soh.Operator.GE: 18, soh.Operator.LE: 65}}`.
527
526
 
528
527
  ### Typed Filter Models
@@ -569,6 +568,15 @@ Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display fo
569
568
  - `FilterDatetimeRange` — produces UTC-aware datetimes
570
569
  - `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
571
570
 
571
+ #### Datetime columns and SQLModel ≥0.0.45
572
+
573
+ Since SQLModel 0.0.45 a bare `datetime` field (no `sa_type` / `sa_column`) becomes `UTCDateTime` — `TIMESTAMP WITH TIME ZONE` that rejects naive values and returns UTC-aware ones. soh works on both sides of this change:
574
+
575
+ - Filter such columns with `FilterDatetime`, `FilterDatetimeRange`, `TimeFilter` or aware `datetime` values. A naive value is rejected by SQLModel and surfaces as `soh.DatabaseError`.
576
+ - `FilterNaiveDatetime` / `FilterNaiveDatetimeRange` are for columns declared explicitly as naive: `sa_type=DateTime` or `sa_column=Column(DateTime())`.
577
+ - `build_dynamic_meta` reports `UTCDateTime` columns as `datetime`.
578
+ - SQLModel does not migrate existing database columns. Over an existing `timestamp` column a bare `datetime` field reads values as UTC and, when the PostgreSQL session `TimeZone` is not UTC, writes them shifted by its offset. To keep the current behaviour of a bare `datetime` field, declare its database type explicitly: `sa_column=Column(DateTime())` for `timestamp`, `sa_column=Column(DateTime(timezone=True))` for `timestamptz`.
579
+
572
580
  ## Eager Loading
573
581
 
574
582
  The library automatically selects the optimal loading strategy:
@@ -666,8 +674,11 @@ Physical column types are mapped automatically. Unknown types fall back to `stri
666
674
  | `Integer`, `BigInteger`, `SmallInteger` | `integer` |
667
675
  | `Numeric`, `Float` | `float` |
668
676
  | `String`, `Text`, `Enum`, `Interval`, `ARRAY`, `Uuid` | `string` |
677
+ | Unmapped `TypeDecorator` (e.g. SQLModel `UTCDateTime`) | type of its `impl` |
669
678
  | Any other type | `string` (fallback) |
670
679
 
680
+ `Interval` is a `TypeDecorator` over `DateTime`, but its own entry wins, so it stays `string`.
681
+
671
682
  ### Label precedence (per physical column)
672
683
 
673
684
  1. `pg_description` — DBA edit via `COMMENT ON COLUMN` (supports per-role `||` overrides)
@@ -783,6 +794,7 @@ Endpoints can be migrated one at a time. Three strategies per endpoint:
783
794
  ### Operators
784
795
 
785
796
  - `soh.Operator` -- StrEnum of operator names
797
+ - `soh.LogicalOperator` -- StrEnum `AND`/`OR` for the `logical_operator` argument
786
798
  - `soh.SUPPORTED_OPERATORS` -- Dict mapping operator names to SQLAlchemy lambdas
787
799
  - `soh.SPECIAL_OPERATORS` -- Frozenset of operators with extended handling (`{"exists"}`)
788
800
 
@@ -837,8 +849,8 @@ Endpoints can be migrated one at a time. Three strategies per endpoint:
837
849
  ## Development
838
850
 
839
851
  ```bash
840
- pip install -e ".[dev]"
841
- pytest tests/
852
+ uv sync
853
+ uv run pytest tests/
842
854
  ```
843
855
 
844
856
  ### Syncing with Remote
@@ -882,6 +894,15 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
882
894
 
883
895
  ## Changelog
884
896
 
897
+ ### 0.0.8 — 2026-10-09
898
+
899
+ - **SQLModel ≥0.0.45 support** — `build_dynamic_meta` maps an unmapped `TypeDecorator` by its `impl`, so bare `datetime` fields (`UTCDateTime` in SQLModel ≥0.0.45) stay `datetime` instead of falling back to `string`. `Interval` stays `string`. Tested on SQLModel 0.0.42 and 0.0.48. The `sqlmodel>=0.0.22` lower bound is unchanged.
900
+ - Dependency is now `sqlalchemy[asyncio]` — the package imports `sqlalchemy.ext.asyncio`, and SQLAlchemy 2.1 (allowed by SQLModel 0.0.48) no longer installs `greenlet` by default.
901
+ - **LogicalOperator** — `StrEnum` for `logical_operator`. An unknown value now raises `InvalidFilterError`; before, anything except `"AND"` silently meant OR. Plain strings `"AND"`/`"OR"` keep working.
902
+ - Typing: `get_object` / `get_objects` overloads narrow the return type by `columns`, `pagination` and `suspend_error`; the package and tests are clean under the fleet `pyright` config.
903
+ - Test dependencies moved to `[dependency-groups] dev` — `uv sync` installs them.
904
+ - Docs: [Datetime columns and SQLModel ≥0.0.45](#datetime-columns-and-sqlmodel-0045).
905
+
885
906
  ### 0.0.6
886
907
 
887
908
  - **build_dynamic_meta** / **load_pg_comments** / **configure_meta_cache_ttl** / **invalidate_meta_cache** — dynamic UI metadata reader. Builds `TableMeta` and `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` (TTL-cached, default 60s) with fallback to `Column(comment=...)` defaults from the model. Supports per-role label/row_link overrides via extended `||` comment format. For physical columns the label precedence is `pg_description` > `Column(comment=...)` model default. For virtual columns (paths with `.` traversing relationships) and full overrides — pass a fully-specified `ColumnMeta` instance directly in the `columns` list (used as-is, no derivation). Lookup `lookup_dict`/`lookup_path` are derived from FK on `*_lkp` tables by `{schema}_{base}` convention. Type derived from SA column type. DBAs can edit labels via `COMMENT ON COLUMN/TABLE` SQL — frontend reflects changes within cache TTL without redeploy. No sync block, no schema migrations introduced; the application's `db.py` is not touched.
@@ -261,7 +261,7 @@ result = await soh.get_objects(
261
261
  users = await soh.get_objects(
262
262
  session, User,
263
263
  filters={"is_active": {soh.Operator.EQ: True}, "role": {soh.Operator.EQ: "admin"}},
264
- logical_operator="OR",
264
+ logical_operator=soh.LogicalOperator.OR,
265
265
  )
266
266
 
267
267
  # Relationship filters (dot-notation)
@@ -489,6 +489,8 @@ Used by `get_objects`. Nested dicts are auto-flattened via `flatten_filters`:
489
489
 
490
490
  `Operator` is a `StrEnum` — each member equals its string value (`Operator.EQ == "eq"`), so string keys still work but enum members provide type safety and autocompletion.
491
491
 
492
+ `logical_operator` of `get_objects`, `count_objects`, `update_objects` and `delete_objects` takes `soh.LogicalOperator.AND` (default) or `soh.LogicalOperator.OR`; the strings `"AND"`/`"OR"` in any case still work. Any other value raises `InvalidFilterError`, even with `suspend_error=True`.
493
+
492
494
  Multiple operators can be combined on a single field: `{"age": {soh.Operator.GE: 18, soh.Operator.LE: 65}}`.
493
495
 
494
496
  ### Typed Filter Models
@@ -535,6 +537,15 @@ Each date part accepts ISO (`2026-04-01`, `2026-04-01T00:00:00Z`) and display fo
535
537
  - `FilterDatetimeRange` — produces UTC-aware datetimes
536
538
  - `FilterNaiveDatetimeRange` — produces naive (timezone-unaware) datetimes
537
539
 
540
+ #### Datetime columns and SQLModel ≥0.0.45
541
+
542
+ Since SQLModel 0.0.45 a bare `datetime` field (no `sa_type` / `sa_column`) becomes `UTCDateTime` — `TIMESTAMP WITH TIME ZONE` that rejects naive values and returns UTC-aware ones. soh works on both sides of this change:
543
+
544
+ - Filter such columns with `FilterDatetime`, `FilterDatetimeRange`, `TimeFilter` or aware `datetime` values. A naive value is rejected by SQLModel and surfaces as `soh.DatabaseError`.
545
+ - `FilterNaiveDatetime` / `FilterNaiveDatetimeRange` are for columns declared explicitly as naive: `sa_type=DateTime` or `sa_column=Column(DateTime())`.
546
+ - `build_dynamic_meta` reports `UTCDateTime` columns as `datetime`.
547
+ - SQLModel does not migrate existing database columns. Over an existing `timestamp` column a bare `datetime` field reads values as UTC and, when the PostgreSQL session `TimeZone` is not UTC, writes them shifted by its offset. To keep the current behaviour of a bare `datetime` field, declare its database type explicitly: `sa_column=Column(DateTime())` for `timestamp`, `sa_column=Column(DateTime(timezone=True))` for `timestamptz`.
548
+
538
549
  ## Eager Loading
539
550
 
540
551
  The library automatically selects the optimal loading strategy:
@@ -632,8 +643,11 @@ Physical column types are mapped automatically. Unknown types fall back to `stri
632
643
  | `Integer`, `BigInteger`, `SmallInteger` | `integer` |
633
644
  | `Numeric`, `Float` | `float` |
634
645
  | `String`, `Text`, `Enum`, `Interval`, `ARRAY`, `Uuid` | `string` |
646
+ | Unmapped `TypeDecorator` (e.g. SQLModel `UTCDateTime`) | type of its `impl` |
635
647
  | Any other type | `string` (fallback) |
636
648
 
649
+ `Interval` is a `TypeDecorator` over `DateTime`, but its own entry wins, so it stays `string`.
650
+
637
651
  ### Label precedence (per physical column)
638
652
 
639
653
  1. `pg_description` — DBA edit via `COMMENT ON COLUMN` (supports per-role `||` overrides)
@@ -749,6 +763,7 @@ Endpoints can be migrated one at a time. Three strategies per endpoint:
749
763
  ### Operators
750
764
 
751
765
  - `soh.Operator` -- StrEnum of operator names
766
+ - `soh.LogicalOperator` -- StrEnum `AND`/`OR` for the `logical_operator` argument
752
767
  - `soh.SUPPORTED_OPERATORS` -- Dict mapping operator names to SQLAlchemy lambdas
753
768
  - `soh.SPECIAL_OPERATORS` -- Frozenset of operators with extended handling (`{"exists"}`)
754
769
 
@@ -803,8 +818,8 @@ Endpoints can be migrated one at a time. Three strategies per endpoint:
803
818
  ## Development
804
819
 
805
820
  ```bash
806
- pip install -e ".[dev]"
807
- pytest tests/
821
+ uv sync
822
+ uv run pytest tests/
808
823
  ```
809
824
 
810
825
  ### Syncing with Remote
@@ -848,6 +863,15 @@ This project follows [Semantic Versioning](https://semver.org/) (MAJOR.MINOR.PAT
848
863
 
849
864
  ## Changelog
850
865
 
866
+ ### 0.0.8 — 2026-10-09
867
+
868
+ - **SQLModel ≥0.0.45 support** — `build_dynamic_meta` maps an unmapped `TypeDecorator` by its `impl`, so bare `datetime` fields (`UTCDateTime` in SQLModel ≥0.0.45) stay `datetime` instead of falling back to `string`. `Interval` stays `string`. Tested on SQLModel 0.0.42 and 0.0.48. The `sqlmodel>=0.0.22` lower bound is unchanged.
869
+ - Dependency is now `sqlalchemy[asyncio]` — the package imports `sqlalchemy.ext.asyncio`, and SQLAlchemy 2.1 (allowed by SQLModel 0.0.48) no longer installs `greenlet` by default.
870
+ - **LogicalOperator** — `StrEnum` for `logical_operator`. An unknown value now raises `InvalidFilterError`; before, anything except `"AND"` silently meant OR. Plain strings `"AND"`/`"OR"` keep working.
871
+ - Typing: `get_object` / `get_objects` overloads narrow the return type by `columns`, `pagination` and `suspend_error`; the package and tests are clean under the fleet `pyright` config.
872
+ - Test dependencies moved to `[dependency-groups] dev` — `uv sync` installs them.
873
+ - Docs: [Datetime columns and SQLModel ≥0.0.45](#datetime-columns-and-sqlmodel-0045).
874
+
851
875
  ### 0.0.6
852
876
 
853
877
  - **build_dynamic_meta** / **load_pg_comments** / **configure_meta_cache_ttl** / **invalidate_meta_cache** — dynamic UI metadata reader. Builds `TableMeta` and `list[ColumnMeta]` from a SQLModel class by reading PostgreSQL `pg_description` (TTL-cached, default 60s) with fallback to `Column(comment=...)` defaults from the model. Supports per-role label/row_link overrides via extended `||` comment format. For physical columns the label precedence is `pg_description` > `Column(comment=...)` model default. For virtual columns (paths with `.` traversing relationships) and full overrides — pass a fully-specified `ColumnMeta` instance directly in the `columns` list (used as-is, no derivation). Lookup `lookup_dict`/`lookup_path` are derived from FK on `*_lkp` tables by `{schema}_{base}` convention. Type derived from SA column type. DBAs can edit labels via `COMMENT ON COLUMN/TABLE` SQL — frontend reflects changes within cache TTL without redeploy. No sync block, no schema migrations introduced; the application's `db.py` is not touched.
@@ -27,7 +27,7 @@ classifiers = [
27
27
  ]
28
28
  dependencies = [
29
29
  "sqlmodel>=0.0.22",
30
- "sqlalchemy>=2.0.46",
30
+ "sqlalchemy[asyncio]>=2.0.46",
31
31
  "pydantic>=2.12",
32
32
  ]
33
33
 
@@ -39,20 +39,10 @@ Issues = "https://github.com/itstandart/sqlmodel-object-helpers/issues"
39
39
 
40
40
  [project.optional-dependencies]
41
41
  dev = [
42
- "pytest>=7.0",
43
- "pytest-asyncio>=0.21",
44
- "aiosqlite>=0.20",
45
42
  "ruff>=0.8.0",
46
43
  "mypy>=1.13.0",
47
44
  ]
48
45
 
49
- [tool.ruff]
50
- target-version = "py314"
51
- line-length = 120
52
-
53
- [tool.ruff.lint]
54
- select = ["E", "F", "W", "I", "UP", "B", "SIM", "PTH"]
55
-
56
46
  [tool.mypy]
57
47
  python_version = "3.14"
58
48
  strict = true
@@ -62,3 +52,13 @@ path = "src/sqlmodel_object_helpers/__init__.py"
62
52
 
63
53
  [tool.hatch.build.targets.wheel]
64
54
  packages = ["src/sqlmodel_object_helpers"]
55
+
56
+ [dependency-groups]
57
+ dev = [
58
+ "ruff==0.16.10",
59
+ "pyright==1.1.414",
60
+ "tzdata==2026.5",
61
+ "pytest==9.1.1",
62
+ "pytest-asyncio==1.4.0",
63
+ "aiosqlite==0.22.1",
64
+ ]
@@ -0,0 +1,38 @@
1
+ {
2
+ // Общий стандарт python-парка. Проверка типов.
3
+ // Источник: devops/ci-templates/standards/pyrightconfig.json @ v1.11.0
4
+ //
5
+ // Файл одинаков во всех репозиториях и правится ТОЛЬКО в devops/ci-templates.
6
+ // Джоба standards_check сверяет его sha256 и упадёт на любой локальной правке.
7
+ //
8
+ // Локальное исключение — комментарий рядом с кодом:
9
+ // value = obj.attr # pyright: ignore[reportAttributeAccessIssue] причина
10
+ // Если исключение общее для парка — оно едет сюда, а не в проект.
11
+
12
+ // Без этих двух ключей pyright не находит виртуальное окружение uv и считает
13
+ // ненайденным КАЖДЫЙ сторонний импорт: на канарейке это давало 32 ложных
14
+ // reportMissingImports из 47 ошибок.
15
+ "venvPath": ".",
16
+ "venv": ".venv",
17
+
18
+ // pythonVersion намеренно не задан: pyright берёт версию из интерпретатора
19
+ // в .venv, то есть из requires-python конкретного репозитория.
20
+
21
+ // standard — текущий режим pyright по умолчанию. На парке практически
22
+ // не отличается от устаревшего basic (16 против 17 ошибок на канарейке),
23
+ // а strict нереалистичен (297 на той же канарейке).
24
+ "typeCheckingMode": "standard",
25
+
26
+ // "**/.*" — это умолчание самого pyright, и его обязательно повторить здесь:
27
+ // задавая свой exclude, мы умолчание ЗАМЕНЯЕМ, а не дополняем. Без этой
28
+ // строки pyright заходит в скрытые каталоги, и у sro-agent-core, где
29
+ // [tool.uv] cache-dir = ".uv-cache" держит кеш пакетов ВНУТРИ репозитория,
30
+ // он анализировал ещё и распакованные колёса: джоба упиралась в потолок
31
+ // раннера 600 с и не заканчивалась вовсе (пайплайны #8517 и #8518).
32
+ // Сюда же попадают .venv, .git, .ruff_cache, .pytest_cache.
33
+ "exclude": [
34
+ "**/.*",
35
+ "**/node_modules",
36
+ "**/__pycache__"
37
+ ]
38
+ }
@@ -0,0 +1,129 @@
1
+ # Общий стандарт python-парка. Линтер и форматтер.
2
+ # Источник: devops/ci-templates/standards/ruff.toml @ v1.25.0
3
+ #
4
+ # Файл одинаков во всех репозиториях и правится ТОЛЬКО в devops/ci-templates.
5
+ # Джоба standards_check сверяет его sha256 и упадёт на любой локальной правке.
6
+ #
7
+ # Локальное исключение — noqa рядом с кодом и с причиной:
8
+ # result = query.all() # noqa: S608 имя колонки только из whitelist
9
+ # # ruff: noqa: UP037 в шапке файла — на весь файл
10
+ # Правило PGH004 не даст поставить noqa без кодов, RUF100 снимет протухшие.
11
+ # Если исключение общее для парка — оно едет сюда, а не в проект.
12
+
13
+ line-length = 120
14
+
15
+ # target-version намеренно не задан: ruff берёт его из project.requires-python
16
+ # в pyproject.toml каждого репозитория (проверено — работает и когда конфиг
17
+ # лежит в отдельном ruff.toml). Так один общий файл остаётся корректным и для
18
+ # библиотеки, объявляющей поддержку более старого Python.
19
+
20
+ [lint]
21
+ select = [
22
+ # pycodestyle + pyflakes — база
23
+ "E", "W", "F",
24
+ # сортировка импортов
25
+ "I",
26
+ # современный синтаксис под target-version
27
+ "UP",
28
+ # типовые дефекты и упрощения
29
+ "B", "SIM", "RET", "C4", "PIE", "PERF", "FURB",
30
+ # сложность функций, порог в [lint.mccabe]
31
+ "C901",
32
+ # pathlib вместо os.path
33
+ "PTH",
34
+ # безопасность (bandit)
35
+ "S",
36
+ # datetime без таймзоны
37
+ "DTZ",
38
+ # логирование
39
+ "LOG", "G",
40
+ # забытый print/pprint
41
+ "T20",
42
+ # корректность async
43
+ "ASYNC",
44
+ # except без типа
45
+ "BLE",
46
+ "INT", "YTT",
47
+ # RUF100 — протухшие noqa
48
+ "RUF",
49
+ # PGH004 — noqa обязан быть с кодами
50
+ "PGH",
51
+ ]
52
+
53
+ ignore = [
54
+ # --- Конфликтуют с ruff format, держать выключенными ---
55
+ # (длину строки контролирует форматтер, отступы и табы — тоже)
56
+ "E501", "W191", "E101",
57
+
58
+ # --- Кириллица ---
59
+ # RUF001-003 считают кириллические буквы «двусмысленными» из-за похожих
60
+ # латинских. Для русскоязычного кода это сплошной шум: 2542 находки из 4690
61
+ # по парку на момент введения стандарта.
62
+ "RUF001", "RUF002", "RUF003",
63
+
64
+ # --- Порядок импортов ---
65
+ # settings и логгер поднимаются до импорта приложения — это осознанный
66
+ # порядок инициализации, а не небрежность.
67
+ "E402",
68
+ ]
69
+
70
+ [lint.mccabe]
71
+ # C901 — единственное измерение, которого в стандарте не было: ветвистость
72
+ # функции ruff до сих пор не мерил вовсе, и её замечал только человек на ревью.
73
+ #
74
+ # Порог 15, а не дефолтные 10. На 10 в exam-crm-core 70 функций, в
75
+ # sro-agent-core 17 — это фон, а не список к работе. На 15 остаётся 32 и 4,
76
+ # и это ровно те места, куда никто не хочет лезть: migrate_fias_addresses (46),
77
+ # recalculate_payments_new_with_session (45), set_block (44), get_objects (42)
78
+ # в легаси-монолите exam-crm и sa_core_compiler.compile (21) в ядре sro-agent.
79
+ #
80
+ # Правило неблокирующее: ruff_lint идёт с allow_failure, а Code Quality в MR
81
+ # показывает находки только на изменённых строках. То есть порог сторожит
82
+ # НОВЫЕ сложные функции, а не требует переписать легаси. Понижать по мере
83
+ # разбора, отдельными релизами.
84
+ #
85
+ # В per-file-ignores для тестов C901 не нужен: на пороге 15 в **/tests/**
86
+ # обоих ядер ноль находок (проверено до введения правила).
87
+ max-complexity = 15
88
+
89
+ [lint.per-file-ignores]
90
+ # Шаблон именно **/tests/**, а не tests/**: у exam-crm-core тесты лежат
91
+ # в app/tests/, и корневой шаблон бы их не поймал.
92
+ "**/tests/**" = [
93
+ # assert — это и есть тест
94
+ "S101",
95
+ # white-box-доступ к приватным атрибутам проверяемого объекта
96
+ "SLF001",
97
+ # магические значения в ожиданиях
98
+ "PLR2004",
99
+ # неиспользуемые аргументы фикстур и фейков, реализующих Protocol
100
+ "ARG",
101
+ # ленивые импорты внутри фикстур
102
+ "PLC0415",
103
+ # составные assert
104
+ "PT018",
105
+ # тестовым пакетам __init__.py не нужен
106
+ "INP001",
107
+ # пароли и токены в фикстурах — синтетика
108
+ "S105", "S106",
109
+ # naive datetime в ожиданиях
110
+ "DTZ001",
111
+ # __enter__ у тест-дублей возвращает self
112
+ "PYI034",
113
+ # bool-параметры в parametrize
114
+ "FBT001", "FBT002",
115
+ # закомментированный код в ожидаемых значениях
116
+ "ERA001",
117
+ # диагностический print
118
+ "T20",
119
+ # широкий except в проверках устойчивости
120
+ "BLE001",
121
+ ]
122
+
123
+ [format]
124
+ # ruff format умеет форматировать python-блоки внутри markdown, и по умолчанию
125
+ # это включено. В парке 36 файлов .md с такими блоками (17 в exam-crm-core,
126
+ # 11 в sro-agent-core): формат переписал бы примеры в документации, а
127
+ # ruff format --check краснел бы на сокращённых и псевдокодовых сниппетах.
128
+ # Стандарт касается кода, а не документации.
129
+ exclude = ["*.md"]
@@ -1,6 +1,6 @@
1
1
  """sqlmodel-object-helpers — reusable query helpers for SQLModel projects."""
2
2
 
3
- __version__ = "0.0.6"
3
+ __version__ = "0.0.8"
4
4
 
5
5
  from .constants import QueryHelperSettings, settings
6
6
  from .dynamic_meta import (
@@ -29,7 +29,7 @@ from .mutations import (
29
29
  update_object,
30
30
  update_objects,
31
31
  )
32
- from .operators import SPECIAL_OPERATORS, SUPPORTED_OPERATORS, Operator
32
+ from .operators import SPECIAL_OPERATORS, SUPPORTED_OPERATORS, LogicalOperator, Operator
33
33
  from .query import count_objects, exists_object, get_object, get_objects, get_projection
34
34
  from .session import auto_session, configure, create_session_dependency
35
35
  from .types.columns import BoolLabels, ColumnMeta, ColumnType, TableMeta
@@ -61,28 +61,14 @@ from .types.pagination import (
61
61
  from .types.projections import ColumnSpec
62
62
 
63
63
  __all__ = [
64
- "add_object",
65
- "add_objects",
66
- "auto_session",
67
- "build_dynamic_meta",
68
- "build_filter",
69
- "build_flat_filter",
70
- "build_load_chain",
71
- "build_load_options",
72
- "check_for_related_records",
64
+ "SPECIAL_OPERATORS",
65
+ "SUPPORTED_OPERATORS",
73
66
  "BoolLabels",
74
67
  "ColumnEntry",
75
68
  "ColumnMeta",
76
69
  "ColumnSpec",
77
70
  "ColumnType",
78
- "configure",
79
- "configure_meta_cache_ttl",
80
- "count_objects",
81
- "create_session_dependency",
82
71
  "DatabaseError",
83
- "delete_object",
84
- "delete_objects",
85
- "exists_object",
86
72
  "FilterBool",
87
73
  "FilterDate",
88
74
  "FilterDatetime",
@@ -93,16 +79,11 @@ __all__ = [
93
79
  "FilterNaiveDatetimeRange",
94
80
  "FilterStr",
95
81
  "FilterTimedelta",
96
- "flatten_filters",
97
- "get_object",
98
- "get_objects",
99
- "get_projection",
100
82
  "GetAllPagination",
101
- "invalidate_meta_cache",
102
83
  "InvalidFilterError",
103
84
  "InvalidLoadPathError",
104
- "load_pg_comments",
105
85
  "LogicalFilter",
86
+ "LogicalOperator",
106
87
  "LookupMeta",
107
88
  "LookupResponse",
108
89
  "MutationError",
@@ -115,12 +96,32 @@ __all__ = [
115
96
  "PaginationR",
116
97
  "QueryError",
117
98
  "QueryHelperSettings",
118
- "settings",
119
- "SPECIAL_OPERATORS",
120
- "SUPPORTED_OPERATORS",
121
99
  "TableMeta",
122
100
  "TimeFilter",
101
+ "UTCDatetime",
102
+ "add_object",
103
+ "add_objects",
104
+ "auto_session",
105
+ "build_dynamic_meta",
106
+ "build_filter",
107
+ "build_flat_filter",
108
+ "build_load_chain",
109
+ "build_load_options",
110
+ "check_for_related_records",
111
+ "configure",
112
+ "configure_meta_cache_ttl",
113
+ "count_objects",
114
+ "create_session_dependency",
115
+ "delete_object",
116
+ "delete_objects",
117
+ "exists_object",
118
+ "flatten_filters",
119
+ "get_object",
120
+ "get_objects",
121
+ "get_projection",
122
+ "invalidate_meta_cache",
123
+ "load_pg_comments",
124
+ "settings",
123
125
  "update_object",
124
126
  "update_objects",
125
- "UTCDatetime",
126
127
  ]