sqlmodel-ext 0.4.0__tar.gz → 0.4.1__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.
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/PKG-INFO +44 -29
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/README.md +42 -27
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/README_zh.md +40 -25
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/cached-table.md +20 -16
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/crud-pipeline.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/metaclass.md +375 -319
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/polymorphic-internals.md +24 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-load-checker.md +33 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/cache-queries.md +143 -142
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-sti-models.md +132 -132
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/handle-concurrent-updates.md +94 -88
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/integrate-with-fastapi.md +10 -7
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/prevent-missing-greenlet.md +167 -162
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/index.md +4 -4
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/base-classes.md +22 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/crud-methods.md +7 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/field-types.md +4 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/index.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/mixins.md +35 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/pagination-types.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/01-getting-started.md +6 -5
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/02-building-a-blog-api.md +389 -388
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/03-adding-redis-cache.md +277 -269
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/cached-table.md +16 -16
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/crud-pipeline.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/metaclass.md +351 -319
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/polymorphic-internals.md +13 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/relation-load-checker.md +21 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/cache-queries.md +143 -142
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/define-sti-models.md +132 -132
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/handle-concurrent-updates.md +93 -87
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/integrate-with-fastapi.md +10 -7
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/prevent-missing-greenlet.md +164 -162
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/index.md +4 -4
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/base-classes.md +19 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/crud-methods.md +5 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/field-types.md +4 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/index.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/mixins.md +30 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/pagination-types.md +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/01-getting-started.md +6 -5
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/02-building-a-blog-api.md +388 -388
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/03-adding-redis-cache.md +277 -269
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/examples/consumer_integration/run.py +149 -148
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/pyproject.toml +2 -2
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/__init__.py +1 -1
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/base.py +127 -3
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/__init__.py +53 -1
- sqlmodel_ext-0.4.1/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +218 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/table.py +70 -13
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/_models.py +69 -67
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_array_explicit_field_sa_type.py +18 -4
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_basic_crud.py +71 -61
- sqlmodel_ext-0.4.1/tests/test_cached_table.py +513 -0
- sqlmodel_ext-0.4.1/tests/test_crud_extended.py +582 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_misc.py +189 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_str_num.py +400 -0
- sqlmodel_ext-0.4.1/tests/test_field_types_url_ssrf.py +267 -0
- sqlmodel_ext-0.4.1/tests/test_metaclass_extra.py +194 -0
- sqlmodel_ext-0.4.1/tests/test_optimistic_lock.py +190 -0
- sqlmodel_ext-0.4.1/tests/test_pagination.py +259 -0
- sqlmodel_ext-0.4.1/tests/test_pg_dialect_types.py +427 -0
- sqlmodel_ext-0.4.1/tests/test_polymorphic_jti.py +273 -0
- sqlmodel_ext-0.4.1/tests/test_polymorphic_sti_query.py +418 -0
- sqlmodel_ext-0.4.1/tests/test_relation_load_checker.py +232 -0
- sqlmodel_ext-0.4.1/tests/test_relation_preload.py +240 -0
- sqlmodel_ext-0.4.1/tests/test_relationship_pep604.py +115 -0
- sqlmodel_ext-0.4.0/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -119
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/.github/workflows/ci.yml +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/.gitignore +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/LICENSE +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/field-types.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/reference/decorators.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/en/tutorials/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/field-types.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/reference/decorators.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/docs/tutorials/index.md +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/package.json +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_compat.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_sa_type.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/cached_table.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/pagination.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/relation_load_checker.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/src/sqlmodel_ext/session.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/__init__.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/conftest.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_all_fields_optional_constraints.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_annotated_default_factory_inheritance.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_decimal_types.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_deferred_index.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_enhanced_session.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_smoke_imports.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/tests/test_sti_default_isolation.py +0 -0
- {sqlmodel_ext-0.4.0 → sqlmodel_ext-0.4.1}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphic inheritance, optimistic locking, relation preloading, and reusable field types.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Foxerine/sqlmodel-ext
|
|
6
6
|
Project-URL: Repository, https://github.com/Foxerine/sqlmodel-ext
|
|
@@ -24,7 +24,7 @@ Classifier: Typing :: Typed
|
|
|
24
24
|
Requires-Python: >=3.12
|
|
25
25
|
Requires-Dist: pydantic>=2.0
|
|
26
26
|
Requires-Dist: sqlalchemy>=2.0
|
|
27
|
-
Requires-Dist: sqlmodel>=0.0.
|
|
27
|
+
Requires-Dist: sqlmodel>=0.0.32
|
|
28
28
|
Provides-Extra: cache
|
|
29
29
|
Requires-Dist: orjson>=3.0; extra == 'cache'
|
|
30
30
|
Requires-Dist: redis>=5.0; extra == 'cache'
|
|
@@ -70,7 +70,7 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
70
70
|
| **RelationPreloadMixin** | Decorator-based automatic relationship preloading (prevents `MissingGreenlet` errors) |
|
|
71
71
|
| **ListResponse[T]** | Generic paginated response model for list endpoints |
|
|
72
72
|
| **Field Types** | Reusable constrained types: `Str64`, `Port`, `IPAddress`, `HttpUrl`, `SafeHttpUrl`, and more |
|
|
73
|
-
| **PostgreSQL Types** | `Array[T]` for native ARRAY, `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
73
|
+
| **PostgreSQL Types** | `Array[T]` for native ARRAY (read-tolerant for enum arrays during rolling-deploy version skew), `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
74
74
|
| **Info Response DTOs** | Pre-built mixins for API response models with id/timestamp fields |
|
|
75
75
|
|
|
76
76
|
## Installation
|
|
@@ -108,12 +108,13 @@ pip install sqlmodel-ext[pgvector]
|
|
|
108
108
|
### Define Models
|
|
109
109
|
|
|
110
110
|
```python
|
|
111
|
-
from
|
|
111
|
+
from pydantic import EmailStr # requires: pip install 'pydantic[email]'
|
|
112
|
+
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
|
|
112
113
|
|
|
113
114
|
# Base class -- fields only, no database table
|
|
114
115
|
class UserBase(SQLModelBase):
|
|
115
|
-
name:
|
|
116
|
-
email:
|
|
116
|
+
name: NonEmptyStrippedStr64 # user-visible name: rejects "" and whitespace-only
|
|
117
|
+
email: EmailStr
|
|
117
118
|
|
|
118
119
|
# Table class -- inherits fields + gains async CRUD + UUID primary key
|
|
119
120
|
class User(UserBase, UUIDTableBaseMixin, table=True):
|
|
@@ -438,17 +439,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
|
|
|
438
439
|
|
|
439
440
|
```python
|
|
440
441
|
from abc import ABC, abstractmethod
|
|
442
|
+
from pydantic import EmailStr
|
|
441
443
|
from sqlmodel_ext import (
|
|
442
444
|
SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
|
|
443
445
|
AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
|
|
444
446
|
ListResponse, TableViewRequest,
|
|
447
|
+
Str512, Text1K,
|
|
445
448
|
)
|
|
446
449
|
|
|
447
450
|
# ── Polymorphic models ────────────────────────────────────────────
|
|
448
451
|
|
|
449
452
|
class NotificationBase(SQLModelBase):
|
|
450
453
|
user_id: UUID = Field(foreign_key='user.id')
|
|
451
|
-
message:
|
|
454
|
+
message: Text1K
|
|
452
455
|
|
|
453
456
|
class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
454
457
|
@abstractmethod
|
|
@@ -457,13 +460,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
|
|
|
457
460
|
NotifSubclassId = create_subclass_id_mixin('notification')
|
|
458
461
|
|
|
459
462
|
class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
460
|
-
email_to:
|
|
463
|
+
email_to: EmailStr
|
|
461
464
|
|
|
462
465
|
def summary(self) -> str:
|
|
463
466
|
return f"Email to {self.email_to}: {self.message}"
|
|
464
467
|
|
|
465
468
|
class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
466
|
-
device_token:
|
|
469
|
+
device_token: Str512
|
|
467
470
|
|
|
468
471
|
def summary(self) -> str:
|
|
469
472
|
return f"Push to {self.device_token}: {self.message}"
|
|
@@ -493,15 +496,15 @@ These mixins provide the async CRUD interface. `TableBaseMixin` uses an auto-inc
|
|
|
493
496
|
Both mixins automatically add `id`, `created_at`, and `updated_at` fields.
|
|
494
497
|
|
|
495
498
|
```python
|
|
496
|
-
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
|
|
499
|
+
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
|
|
497
500
|
|
|
498
501
|
# Integer primary key
|
|
499
502
|
class LogEntry(SQLModelBase, TableBaseMixin, table=True):
|
|
500
|
-
message:
|
|
503
|
+
message: Text1K
|
|
501
504
|
|
|
502
505
|
# UUID primary key (recommended for most use cases)
|
|
503
506
|
class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
504
|
-
name:
|
|
507
|
+
name: NonEmptyStrippedStr64
|
|
505
508
|
```
|
|
506
509
|
|
|
507
510
|
#### `add()` -- Batch Insert
|
|
@@ -529,7 +532,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
|
|
|
529
532
|
user = await user.save(session)
|
|
530
533
|
|
|
531
534
|
# Save with relationship preloading
|
|
532
|
-
|
|
535
|
+
# (rel() casts the Relationship field for type checkers, which would
|
|
536
|
+
# otherwise infer User.profile as Profile instead of a loadable attribute)
|
|
537
|
+
user = await user.save(session, load=rel(User.profile))
|
|
533
538
|
|
|
534
539
|
# Save with optimistic lock retry
|
|
535
540
|
user = await user.save(session, optimistic_retry_count=3)
|
|
@@ -562,9 +567,10 @@ user3 = await user3.save(session) # commits all three
|
|
|
562
567
|
#### `update()` -- Partial Update from a Model
|
|
563
568
|
|
|
564
569
|
```python
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
570
|
+
# all_fields_optional auto-converts every inherited field to ``T | None = None``
|
|
571
|
+
# while preserving constraints -- no hand-written Optional boilerplate
|
|
572
|
+
class UserUpdate(UserBase, all_fields_optional=True):
|
|
573
|
+
pass
|
|
568
574
|
|
|
569
575
|
# Only updates fields that were explicitly set
|
|
570
576
|
user = await user.update(session, UserUpdate(name="Charlie"))
|
|
@@ -643,7 +649,7 @@ users = await User.get(session, fetch_mode="all")
|
|
|
643
649
|
user = await User.get(
|
|
644
650
|
session,
|
|
645
651
|
User.id == user_id,
|
|
646
|
-
load=[User.profile, User.orders],
|
|
652
|
+
load=[rel(User.profile), rel(User.orders)],
|
|
647
653
|
)
|
|
648
654
|
|
|
649
655
|
# With JOIN
|
|
@@ -767,11 +773,12 @@ from sqlmodel_ext import (
|
|
|
767
773
|
SQLModelBase, UUIDTableBaseMixin,
|
|
768
774
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
769
775
|
create_subclass_id_mixin,
|
|
776
|
+
HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
|
|
770
777
|
)
|
|
771
778
|
|
|
772
779
|
# 1. Base class (fields only, no table)
|
|
773
780
|
class ToolBase(SQLModelBase):
|
|
774
|
-
name:
|
|
781
|
+
name: NonEmptyStrippedStr64
|
|
775
782
|
|
|
776
783
|
# 2. Abstract parent (creates the parent table)
|
|
777
784
|
class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
@@ -783,13 +790,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
|
|
|
783
790
|
|
|
784
791
|
# 4. Concrete subclasses (each gets its own table)
|
|
785
792
|
class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
786
|
-
search_url:
|
|
793
|
+
search_url: HttpUrl
|
|
787
794
|
|
|
788
795
|
async def execute(self) -> str:
|
|
789
796
|
return f"Searching {self.search_url}"
|
|
790
797
|
|
|
791
798
|
class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
792
|
-
precision:
|
|
799
|
+
precision: NonNegativeInt = 2
|
|
793
800
|
|
|
794
801
|
async def execute(self) -> str:
|
|
795
802
|
return "Calculating..."
|
|
@@ -823,16 +830,17 @@ from sqlmodel_ext import (
|
|
|
823
830
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
824
831
|
register_sti_columns_for_all_subclasses,
|
|
825
832
|
register_sti_column_properties_for_all_subclasses,
|
|
833
|
+
NonNegativeBigInt, Str256,
|
|
826
834
|
)
|
|
827
835
|
|
|
828
836
|
class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
|
|
829
|
-
filename:
|
|
837
|
+
filename: Str256
|
|
830
838
|
|
|
831
839
|
class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
832
840
|
upload_deadline: datetime | None = None # Added to userfile table as nullable
|
|
833
841
|
|
|
834
842
|
class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
835
|
-
file_size:
|
|
843
|
+
file_size: NonNegativeBigInt | None = None # Added to userfile table as nullable
|
|
836
844
|
|
|
837
845
|
# After all models are defined, before configure_mappers():
|
|
838
846
|
register_sti_columns_for_all_subclasses()
|
|
@@ -858,7 +866,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
|
858
866
|
tool_set = await ToolSet.get(
|
|
859
867
|
session,
|
|
860
868
|
ToolSet.id == ts_id,
|
|
861
|
-
load=ToolSet.tools,
|
|
869
|
+
load=rel(ToolSet.tools),
|
|
862
870
|
jti_subclasses='all', # Loads all subclass-specific columns
|
|
863
871
|
)
|
|
864
872
|
```
|
|
@@ -885,15 +893,22 @@ Tool._is_joined_table_inheritance() # True for JTI, False for STI
|
|
|
885
893
|
Prevents lost updates in concurrent environments using SQLAlchemy's `version_id_col` mechanism.
|
|
886
894
|
|
|
887
895
|
```python
|
|
896
|
+
from enum import StrEnum
|
|
897
|
+
|
|
888
898
|
from sqlmodel_ext import (
|
|
889
899
|
SQLModelBase, UUIDTableBaseMixin,
|
|
890
900
|
OptimisticLockMixin, OptimisticLockError,
|
|
901
|
+
NonNegativeDecimal38_18,
|
|
891
902
|
)
|
|
892
903
|
|
|
904
|
+
class OrderStatusEnum(StrEnum):
|
|
905
|
+
pending = 'pending'
|
|
906
|
+
paid = 'paid'
|
|
907
|
+
|
|
893
908
|
# OptimisticLockMixin MUST come before TableBaseMixin in MRO
|
|
894
909
|
class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
|
|
895
|
-
status:
|
|
896
|
-
amount:
|
|
910
|
+
status: OrderStatusEnum = OrderStatusEnum.pending
|
|
911
|
+
amount: NonNegativeDecimal38_18
|
|
897
912
|
```
|
|
898
913
|
|
|
899
914
|
The mixin adds a `version` integer field (starting at 0). Every `UPDATE` generates SQL like:
|
|
@@ -945,11 +960,11 @@ The `RelationPreloadMixin` and `@requires_relations` decorator automatically loa
|
|
|
945
960
|
|
|
946
961
|
```python
|
|
947
962
|
from sqlmodel import Relationship
|
|
948
|
-
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
|
|
963
|
+
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
|
|
949
964
|
from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
|
|
950
965
|
|
|
951
966
|
class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
952
|
-
price:
|
|
967
|
+
price: NonNegativeDecimal38_18
|
|
953
968
|
|
|
954
969
|
class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
955
970
|
config: GeneratorConfig = Relationship()
|
|
@@ -1391,12 +1406,12 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
|
|
|
1391
1406
|
|
|
1392
1407
|
```python
|
|
1393
1408
|
class UserBase(SQLModelBase):
|
|
1394
|
-
name:
|
|
1409
|
+
name: NonEmptyStrippedStr64
|
|
1395
1410
|
"""User display name""" # ← parsed by Pydantic
|
|
1396
1411
|
|
|
1397
1412
|
class UserUpdateRequest(UserBase, all_fields_optional=True):
|
|
1398
1413
|
pass
|
|
1399
|
-
# name:
|
|
1414
|
+
# name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
|
|
1400
1415
|
# Shows up correctly in OpenAPI/Swagger docs
|
|
1401
1416
|
```
|
|
1402
1417
|
|
|
@@ -25,7 +25,7 @@ Extended SQLModel infrastructure: smart metaclass, async CRUD mixins, polymorphi
|
|
|
25
25
|
| **RelationPreloadMixin** | Decorator-based automatic relationship preloading (prevents `MissingGreenlet` errors) |
|
|
26
26
|
| **ListResponse[T]** | Generic paginated response model for list endpoints |
|
|
27
27
|
| **Field Types** | Reusable constrained types: `Str64`, `Port`, `IPAddress`, `HttpUrl`, `SafeHttpUrl`, and more |
|
|
28
|
-
| **PostgreSQL Types** | `Array[T]` for native ARRAY, `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
28
|
+
| **PostgreSQL Types** | `Array[T]` for native ARRAY (read-tolerant for enum arrays during rolling-deploy version skew), `JSON100K`/`JSONList100K` for size-limited JSONB, `NumpyVector` for pgvector+NumPy |
|
|
29
29
|
| **Info Response DTOs** | Pre-built mixins for API response models with id/timestamp fields |
|
|
30
30
|
|
|
31
31
|
## Installation
|
|
@@ -63,12 +63,13 @@ pip install sqlmodel-ext[pgvector]
|
|
|
63
63
|
### Define Models
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
|
-
from
|
|
66
|
+
from pydantic import EmailStr # requires: pip install 'pydantic[email]'
|
|
67
|
+
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
|
|
67
68
|
|
|
68
69
|
# Base class -- fields only, no database table
|
|
69
70
|
class UserBase(SQLModelBase):
|
|
70
|
-
name:
|
|
71
|
-
email:
|
|
71
|
+
name: NonEmptyStrippedStr64 # user-visible name: rejects "" and whitespace-only
|
|
72
|
+
email: EmailStr
|
|
72
73
|
|
|
73
74
|
# Table class -- inherits fields + gains async CRUD + UUID primary key
|
|
74
75
|
class User(UserBase, UUIDTableBaseMixin, table=True):
|
|
@@ -393,17 +394,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
|
|
|
393
394
|
|
|
394
395
|
```python
|
|
395
396
|
from abc import ABC, abstractmethod
|
|
397
|
+
from pydantic import EmailStr
|
|
396
398
|
from sqlmodel_ext import (
|
|
397
399
|
SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
|
|
398
400
|
AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
|
|
399
401
|
ListResponse, TableViewRequest,
|
|
402
|
+
Str512, Text1K,
|
|
400
403
|
)
|
|
401
404
|
|
|
402
405
|
# ── Polymorphic models ────────────────────────────────────────────
|
|
403
406
|
|
|
404
407
|
class NotificationBase(SQLModelBase):
|
|
405
408
|
user_id: UUID = Field(foreign_key='user.id')
|
|
406
|
-
message:
|
|
409
|
+
message: Text1K
|
|
407
410
|
|
|
408
411
|
class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
409
412
|
@abstractmethod
|
|
@@ -412,13 +415,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
|
|
|
412
415
|
NotifSubclassId = create_subclass_id_mixin('notification')
|
|
413
416
|
|
|
414
417
|
class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
415
|
-
email_to:
|
|
418
|
+
email_to: EmailStr
|
|
416
419
|
|
|
417
420
|
def summary(self) -> str:
|
|
418
421
|
return f"Email to {self.email_to}: {self.message}"
|
|
419
422
|
|
|
420
423
|
class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
421
|
-
device_token:
|
|
424
|
+
device_token: Str512
|
|
422
425
|
|
|
423
426
|
def summary(self) -> str:
|
|
424
427
|
return f"Push to {self.device_token}: {self.message}"
|
|
@@ -448,15 +451,15 @@ These mixins provide the async CRUD interface. `TableBaseMixin` uses an auto-inc
|
|
|
448
451
|
Both mixins automatically add `id`, `created_at`, and `updated_at` fields.
|
|
449
452
|
|
|
450
453
|
```python
|
|
451
|
-
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
|
|
454
|
+
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
|
|
452
455
|
|
|
453
456
|
# Integer primary key
|
|
454
457
|
class LogEntry(SQLModelBase, TableBaseMixin, table=True):
|
|
455
|
-
message:
|
|
458
|
+
message: Text1K
|
|
456
459
|
|
|
457
460
|
# UUID primary key (recommended for most use cases)
|
|
458
461
|
class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
459
|
-
name:
|
|
462
|
+
name: NonEmptyStrippedStr64
|
|
460
463
|
```
|
|
461
464
|
|
|
462
465
|
#### `add()` -- Batch Insert
|
|
@@ -484,7 +487,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
|
|
|
484
487
|
user = await user.save(session)
|
|
485
488
|
|
|
486
489
|
# Save with relationship preloading
|
|
487
|
-
|
|
490
|
+
# (rel() casts the Relationship field for type checkers, which would
|
|
491
|
+
# otherwise infer User.profile as Profile instead of a loadable attribute)
|
|
492
|
+
user = await user.save(session, load=rel(User.profile))
|
|
488
493
|
|
|
489
494
|
# Save with optimistic lock retry
|
|
490
495
|
user = await user.save(session, optimistic_retry_count=3)
|
|
@@ -517,9 +522,10 @@ user3 = await user3.save(session) # commits all three
|
|
|
517
522
|
#### `update()` -- Partial Update from a Model
|
|
518
523
|
|
|
519
524
|
```python
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
525
|
+
# all_fields_optional auto-converts every inherited field to ``T | None = None``
|
|
526
|
+
# while preserving constraints -- no hand-written Optional boilerplate
|
|
527
|
+
class UserUpdate(UserBase, all_fields_optional=True):
|
|
528
|
+
pass
|
|
523
529
|
|
|
524
530
|
# Only updates fields that were explicitly set
|
|
525
531
|
user = await user.update(session, UserUpdate(name="Charlie"))
|
|
@@ -598,7 +604,7 @@ users = await User.get(session, fetch_mode="all")
|
|
|
598
604
|
user = await User.get(
|
|
599
605
|
session,
|
|
600
606
|
User.id == user_id,
|
|
601
|
-
load=[User.profile, User.orders],
|
|
607
|
+
load=[rel(User.profile), rel(User.orders)],
|
|
602
608
|
)
|
|
603
609
|
|
|
604
610
|
# With JOIN
|
|
@@ -722,11 +728,12 @@ from sqlmodel_ext import (
|
|
|
722
728
|
SQLModelBase, UUIDTableBaseMixin,
|
|
723
729
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
724
730
|
create_subclass_id_mixin,
|
|
731
|
+
HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
|
|
725
732
|
)
|
|
726
733
|
|
|
727
734
|
# 1. Base class (fields only, no table)
|
|
728
735
|
class ToolBase(SQLModelBase):
|
|
729
|
-
name:
|
|
736
|
+
name: NonEmptyStrippedStr64
|
|
730
737
|
|
|
731
738
|
# 2. Abstract parent (creates the parent table)
|
|
732
739
|
class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
@@ -738,13 +745,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
|
|
|
738
745
|
|
|
739
746
|
# 4. Concrete subclasses (each gets its own table)
|
|
740
747
|
class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
741
|
-
search_url:
|
|
748
|
+
search_url: HttpUrl
|
|
742
749
|
|
|
743
750
|
async def execute(self) -> str:
|
|
744
751
|
return f"Searching {self.search_url}"
|
|
745
752
|
|
|
746
753
|
class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
747
|
-
precision:
|
|
754
|
+
precision: NonNegativeInt = 2
|
|
748
755
|
|
|
749
756
|
async def execute(self) -> str:
|
|
750
757
|
return "Calculating..."
|
|
@@ -778,16 +785,17 @@ from sqlmodel_ext import (
|
|
|
778
785
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
779
786
|
register_sti_columns_for_all_subclasses,
|
|
780
787
|
register_sti_column_properties_for_all_subclasses,
|
|
788
|
+
NonNegativeBigInt, Str256,
|
|
781
789
|
)
|
|
782
790
|
|
|
783
791
|
class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
|
|
784
|
-
filename:
|
|
792
|
+
filename: Str256
|
|
785
793
|
|
|
786
794
|
class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
787
795
|
upload_deadline: datetime | None = None # Added to userfile table as nullable
|
|
788
796
|
|
|
789
797
|
class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
790
|
-
file_size:
|
|
798
|
+
file_size: NonNegativeBigInt | None = None # Added to userfile table as nullable
|
|
791
799
|
|
|
792
800
|
# After all models are defined, before configure_mappers():
|
|
793
801
|
register_sti_columns_for_all_subclasses()
|
|
@@ -813,7 +821,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
|
813
821
|
tool_set = await ToolSet.get(
|
|
814
822
|
session,
|
|
815
823
|
ToolSet.id == ts_id,
|
|
816
|
-
load=ToolSet.tools,
|
|
824
|
+
load=rel(ToolSet.tools),
|
|
817
825
|
jti_subclasses='all', # Loads all subclass-specific columns
|
|
818
826
|
)
|
|
819
827
|
```
|
|
@@ -840,15 +848,22 @@ Tool._is_joined_table_inheritance() # True for JTI, False for STI
|
|
|
840
848
|
Prevents lost updates in concurrent environments using SQLAlchemy's `version_id_col` mechanism.
|
|
841
849
|
|
|
842
850
|
```python
|
|
851
|
+
from enum import StrEnum
|
|
852
|
+
|
|
843
853
|
from sqlmodel_ext import (
|
|
844
854
|
SQLModelBase, UUIDTableBaseMixin,
|
|
845
855
|
OptimisticLockMixin, OptimisticLockError,
|
|
856
|
+
NonNegativeDecimal38_18,
|
|
846
857
|
)
|
|
847
858
|
|
|
859
|
+
class OrderStatusEnum(StrEnum):
|
|
860
|
+
pending = 'pending'
|
|
861
|
+
paid = 'paid'
|
|
862
|
+
|
|
848
863
|
# OptimisticLockMixin MUST come before TableBaseMixin in MRO
|
|
849
864
|
class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
|
|
850
|
-
status:
|
|
851
|
-
amount:
|
|
865
|
+
status: OrderStatusEnum = OrderStatusEnum.pending
|
|
866
|
+
amount: NonNegativeDecimal38_18
|
|
852
867
|
```
|
|
853
868
|
|
|
854
869
|
The mixin adds a `version` integer field (starting at 0). Every `UPDATE` generates SQL like:
|
|
@@ -900,11 +915,11 @@ The `RelationPreloadMixin` and `@requires_relations` decorator automatically loa
|
|
|
900
915
|
|
|
901
916
|
```python
|
|
902
917
|
from sqlmodel import Relationship
|
|
903
|
-
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
|
|
918
|
+
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
|
|
904
919
|
from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
|
|
905
920
|
|
|
906
921
|
class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
907
|
-
price:
|
|
922
|
+
price: NonNegativeDecimal38_18
|
|
908
923
|
|
|
909
924
|
class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
910
925
|
config: GeneratorConfig = Relationship()
|
|
@@ -1346,12 +1361,12 @@ When using Pydantic's `use_attribute_docstrings=True` (enabled by default in `SQ
|
|
|
1346
1361
|
|
|
1347
1362
|
```python
|
|
1348
1363
|
class UserBase(SQLModelBase):
|
|
1349
|
-
name:
|
|
1364
|
+
name: NonEmptyStrippedStr64
|
|
1350
1365
|
"""User display name""" # ← parsed by Pydantic
|
|
1351
1366
|
|
|
1352
1367
|
class UserUpdateRequest(UserBase, all_fields_optional=True):
|
|
1353
1368
|
pass
|
|
1354
|
-
# name:
|
|
1369
|
+
# name: NonEmptyStrippedStr64 | None = None — description "User display name" is inherited
|
|
1355
1370
|
# Shows up correctly in OpenAPI/Swagger docs
|
|
1356
1371
|
```
|
|
1357
1372
|
|
|
@@ -25,7 +25,7 @@ SQLModel 增强基础设施:智能元类、异步 CRUD Mixin、多态继承、
|
|
|
25
25
|
| **RelationPreloadMixin** | 基于装饰器的关系自动预加载(防止 `MissingGreenlet` 错误) |
|
|
26
26
|
| **ListResponse[T]** | 泛型分页响应模型,适用于列表接口 |
|
|
27
27
|
| **字段类型** | 可复用的约束类型:`Str64`、`Port`、`IPAddress`、`HttpUrl`、`SafeHttpUrl` 等 |
|
|
28
|
-
| **PostgreSQL 类型** | `Array[T]` 原生 ARRAY
|
|
28
|
+
| **PostgreSQL 类型** | `Array[T]` 原生 ARRAY(枚举数组在滚动部署版本偏差窗口读容忍)、`JSON100K`/`JSONList100K` 限长 JSONB、`NumpyVector` pgvector+NumPy 集成 |
|
|
29
29
|
| **响应 DTO Mixin** | 预构建的 API 响应模型 Mixin,包含 id/时间戳字段 |
|
|
30
30
|
|
|
31
31
|
## 安装
|
|
@@ -63,12 +63,13 @@ pip install sqlmodel-ext[pgvector]
|
|
|
63
63
|
### 定义模型
|
|
64
64
|
|
|
65
65
|
```python
|
|
66
|
-
from
|
|
66
|
+
from pydantic import EmailStr # 需要:pip install 'pydantic[email]'
|
|
67
|
+
from sqlmodel_ext import SQLModelBase, UUIDTableBaseMixin, NonEmptyStrippedStr64
|
|
67
68
|
|
|
68
69
|
# Base 类 -- 仅定义字段,不创建数据库表
|
|
69
70
|
class UserBase(SQLModelBase):
|
|
70
|
-
name:
|
|
71
|
-
email:
|
|
71
|
+
name: NonEmptyStrippedStr64 # 用户可见命名:拒绝空串与纯空白
|
|
72
|
+
email: EmailStr
|
|
72
73
|
|
|
73
74
|
# Table 类 -- 继承字段 + 获得异步 CRUD + UUID 主键
|
|
74
75
|
class User(UserBase, UUIDTableBaseMixin, table=True):
|
|
@@ -361,17 +362,19 @@ async def delete_article(session: SessionDep, article_id: UUID) -> None:
|
|
|
361
362
|
|
|
362
363
|
```python
|
|
363
364
|
from abc import ABC, abstractmethod
|
|
365
|
+
from pydantic import EmailStr
|
|
364
366
|
from sqlmodel_ext import (
|
|
365
367
|
SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin,
|
|
366
368
|
AutoPolymorphicIdentityMixin, create_subclass_id_mixin,
|
|
367
369
|
ListResponse, TableViewRequest,
|
|
370
|
+
Str512, Text1K,
|
|
368
371
|
)
|
|
369
372
|
|
|
370
373
|
# ── 多态模型 ─────────────────────────────────────────────────────
|
|
371
374
|
|
|
372
375
|
class NotificationBase(SQLModelBase):
|
|
373
376
|
user_id: UUID = Field(foreign_key='user.id')
|
|
374
|
-
message:
|
|
377
|
+
message: Text1K
|
|
375
378
|
|
|
376
379
|
class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
377
380
|
@abstractmethod
|
|
@@ -380,13 +383,13 @@ class Notification(NotificationBase, UUIDTableBaseMixin, PolymorphicBaseMixin, A
|
|
|
380
383
|
NotifSubclassId = create_subclass_id_mixin('notification')
|
|
381
384
|
|
|
382
385
|
class EmailNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
383
|
-
email_to:
|
|
386
|
+
email_to: EmailStr
|
|
384
387
|
|
|
385
388
|
def summary(self) -> str:
|
|
386
389
|
return f"Email to {self.email_to}: {self.message}"
|
|
387
390
|
|
|
388
391
|
class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMixin, table=True):
|
|
389
|
-
device_token:
|
|
392
|
+
device_token: Str512
|
|
390
393
|
|
|
391
394
|
def summary(self) -> str:
|
|
392
395
|
return f"Push to {self.device_token}: {self.message}"
|
|
@@ -416,15 +419,15 @@ async def list_notifications(
|
|
|
416
419
|
两者均自动添加 `id`、`created_at` 和 `updated_at` 字段。
|
|
417
420
|
|
|
418
421
|
```python
|
|
419
|
-
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin
|
|
422
|
+
from sqlmodel_ext import SQLModelBase, TableBaseMixin, UUIDTableBaseMixin, NonEmptyStrippedStr64, Text1K
|
|
420
423
|
|
|
421
424
|
# 整数主键
|
|
422
425
|
class LogEntry(SQLModelBase, TableBaseMixin, table=True):
|
|
423
|
-
message:
|
|
426
|
+
message: Text1K
|
|
424
427
|
|
|
425
428
|
# UUID 主键(大多数场景推荐)
|
|
426
429
|
class Project(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
427
|
-
name:
|
|
430
|
+
name: NonEmptyStrippedStr64
|
|
428
431
|
```
|
|
429
432
|
|
|
430
433
|
#### `add()` -- 批量插入
|
|
@@ -452,7 +455,9 @@ user = await User.add(session, User(name="Alice", email="a@x.com"))
|
|
|
452
455
|
user = await user.save(session)
|
|
453
456
|
|
|
454
457
|
# 保存后预加载关系
|
|
455
|
-
|
|
458
|
+
# rel() 把 Relationship 字段 cast 为 QueryableAttribute——
|
|
459
|
+
# 否则类型检查器把 User.profile 推断为 Profile 而非可加载属性
|
|
460
|
+
user = await user.save(session, load=rel(User.profile))
|
|
456
461
|
|
|
457
462
|
# 乐观锁自动重试
|
|
458
463
|
user = await user.save(session, optimistic_retry_count=3)
|
|
@@ -485,9 +490,10 @@ user3 = await user3.save(session) # 一次性 commit 全部三条
|
|
|
485
490
|
#### `update()` -- 从模型实例局部更新
|
|
486
491
|
|
|
487
492
|
```python
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
493
|
+
# all_fields_optional 自动把所有继承字段转为 ``T | None = None``
|
|
494
|
+
# 并保留约束——无需手写 Optional 样板
|
|
495
|
+
class UserUpdate(UserBase, all_fields_optional=True):
|
|
496
|
+
pass
|
|
491
497
|
|
|
492
498
|
# 仅更新显式设置的字段
|
|
493
499
|
user = await user.update(session, UserUpdate(name="Charlie"))
|
|
@@ -566,7 +572,7 @@ users = await User.get(session, fetch_mode="all")
|
|
|
566
572
|
user = await User.get(
|
|
567
573
|
session,
|
|
568
574
|
User.id == user_id,
|
|
569
|
-
load=[User.profile, User.orders],
|
|
575
|
+
load=[rel(User.profile), rel(User.orders)],
|
|
570
576
|
)
|
|
571
577
|
|
|
572
578
|
# JOIN 查询
|
|
@@ -690,11 +696,12 @@ from sqlmodel_ext import (
|
|
|
690
696
|
SQLModelBase, UUIDTableBaseMixin,
|
|
691
697
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
692
698
|
create_subclass_id_mixin,
|
|
699
|
+
HttpUrl, NonEmptyStrippedStr64, NonNegativeInt,
|
|
693
700
|
)
|
|
694
701
|
|
|
695
702
|
# 1. Base 类(仅定义字段,无表)
|
|
696
703
|
class ToolBase(SQLModelBase):
|
|
697
|
-
name:
|
|
704
|
+
name: NonEmptyStrippedStr64
|
|
698
705
|
|
|
699
706
|
# 2. 抽象父类(创建父表)
|
|
700
707
|
class Tool(ToolBase, UUIDTableBaseMixin, PolymorphicBaseMixin, ABC):
|
|
@@ -706,13 +713,13 @@ ToolSubclassIdMixin = create_subclass_id_mixin('tool')
|
|
|
706
713
|
|
|
707
714
|
# 4. 具体子类(各自拥有独立表)
|
|
708
715
|
class WebSearchTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
709
|
-
search_url:
|
|
716
|
+
search_url: HttpUrl
|
|
710
717
|
|
|
711
718
|
async def execute(self) -> str:
|
|
712
719
|
return f"Searching {self.search_url}"
|
|
713
720
|
|
|
714
721
|
class CalculatorTool(ToolSubclassIdMixin, Tool, AutoPolymorphicIdentityMixin, table=True):
|
|
715
|
-
precision:
|
|
722
|
+
precision: NonNegativeInt = 2
|
|
716
723
|
|
|
717
724
|
async def execute(self) -> str:
|
|
718
725
|
return "Calculating..."
|
|
@@ -746,16 +753,17 @@ from sqlmodel_ext import (
|
|
|
746
753
|
PolymorphicBaseMixin, AutoPolymorphicIdentityMixin,
|
|
747
754
|
register_sti_columns_for_all_subclasses,
|
|
748
755
|
register_sti_column_properties_for_all_subclasses,
|
|
756
|
+
NonNegativeBigInt, Str256,
|
|
749
757
|
)
|
|
750
758
|
|
|
751
759
|
class UserFile(SQLModelBase, UUIDTableBaseMixin, PolymorphicBaseMixin, table=True):
|
|
752
|
-
filename:
|
|
760
|
+
filename: Str256
|
|
753
761
|
|
|
754
762
|
class PendingFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
755
763
|
upload_deadline: datetime | None = None # 作为 nullable 列添加到 userfile 表
|
|
756
764
|
|
|
757
765
|
class CompletedFile(UserFile, AutoPolymorphicIdentityMixin, table=True):
|
|
758
|
-
file_size:
|
|
766
|
+
file_size: NonNegativeBigInt | None = None # 作为 nullable 列添加到 userfile 表
|
|
759
767
|
|
|
760
768
|
# 所有模型定义完成后,在 configure_mappers() 之前调用:
|
|
761
769
|
register_sti_columns_for_all_subclasses()
|
|
@@ -781,7 +789,7 @@ class ToolSet(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
|
781
789
|
tool_set = await ToolSet.get(
|
|
782
790
|
session,
|
|
783
791
|
ToolSet.id == ts_id,
|
|
784
|
-
load=ToolSet.tools,
|
|
792
|
+
load=rel(ToolSet.tools),
|
|
785
793
|
jti_subclasses='all', # 加载所有子类特有的列
|
|
786
794
|
)
|
|
787
795
|
```
|
|
@@ -808,15 +816,22 @@ Tool._is_joined_table_inheritance() # JTI 返回 True,STI 返回 False
|
|
|
808
816
|
利用 SQLAlchemy 的 `version_id_col` 机制防止并发环境下的更新丢失。
|
|
809
817
|
|
|
810
818
|
```python
|
|
819
|
+
from enum import StrEnum
|
|
820
|
+
|
|
811
821
|
from sqlmodel_ext import (
|
|
812
822
|
SQLModelBase, UUIDTableBaseMixin,
|
|
813
823
|
OptimisticLockMixin, OptimisticLockError,
|
|
824
|
+
NonNegativeDecimal38_18,
|
|
814
825
|
)
|
|
815
826
|
|
|
827
|
+
class OrderStatusEnum(StrEnum):
|
|
828
|
+
pending = 'pending'
|
|
829
|
+
paid = 'paid'
|
|
830
|
+
|
|
816
831
|
# OptimisticLockMixin 在 MRO 中必须位于 TableBaseMixin 之前
|
|
817
832
|
class Order(OptimisticLockMixin, UUIDTableBaseMixin, table=True):
|
|
818
|
-
status:
|
|
819
|
-
amount:
|
|
833
|
+
status: OrderStatusEnum = OrderStatusEnum.pending
|
|
834
|
+
amount: NonNegativeDecimal38_18
|
|
820
835
|
```
|
|
821
836
|
|
|
822
837
|
该 Mixin 添加一个 `version` 整数字段(初始值为 0)。每次 `UPDATE` 生成如下 SQL:
|
|
@@ -868,11 +883,11 @@ order = await order.update(session, update_data, optimistic_retry_count=3)
|
|
|
868
883
|
|
|
869
884
|
```python
|
|
870
885
|
from sqlmodel import Relationship
|
|
871
|
-
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase
|
|
886
|
+
from sqlmodel_ext import UUIDTableBaseMixin, SQLModelBase, NonNegativeDecimal38_18
|
|
872
887
|
from sqlmodel_ext.mixins import RelationPreloadMixin, requires_relations
|
|
873
888
|
|
|
874
889
|
class GeneratorConfig(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
875
|
-
price:
|
|
890
|
+
price: NonNegativeDecimal38_18
|
|
876
891
|
|
|
877
892
|
class Generator(SQLModelBase, UUIDTableBaseMixin, table=True):
|
|
878
893
|
config: GeneratorConfig = Relationship()
|