sqlmodel-ext 0.3.4__tar.gz → 0.3.5__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 (153) hide show
  1. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/PKG-INFO +49 -17
  2. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/README.md +48 -16
  3. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/index.md +0 -9
  4. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/field-types.md +218 -220
  5. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/tutorials/01-getting-started.md +174 -174
  6. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/tutorials/index.md +32 -32
  7. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/index.md +0 -9
  8. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/field-types.md +218 -220
  9. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/tutorials/01-getting-started.md +174 -174
  10. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/tutorials/index.md +32 -32
  11. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/pyproject.toml +1 -1
  12. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/__init__.py +1 -1
  13. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/base.py +55 -4
  14. sqlmodel_ext-0.3.5/tests/test_array_explicit_field_sa_type.py +116 -0
  15. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/.github/workflows/ci.yml +0 -0
  16. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/.gitignore +0 -0
  17. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/LICENSE +0 -0
  18. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/README_zh.md +0 -0
  19. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/.vitepress/config.mts +0 -0
  20. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/.vitepress/en.mts +0 -0
  21. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/.vitepress/theme/index.ts +0 -0
  22. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/.vitepress/theme/style/custom.css +0 -0
  23. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/.vitepress/zh.mts +0 -0
  24. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/cached-table.md +0 -0
  25. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/cascade-delete-semantics.md +0 -0
  26. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/crud-pipeline.md +0 -0
  27. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/index.md +0 -0
  28. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/metaclass.md +0 -0
  29. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/optimistic-lock.md +0 -0
  30. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/polymorphic-internals.md +0 -0
  31. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/prerequisites.md +0 -0
  32. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/relation-load-checker.md +0 -0
  33. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/explanation/relation-preload.md +0 -0
  34. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/cached-table.md +0 -0
  35. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/crud.md +0 -0
  36. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/field-types.md +0 -0
  37. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/index.md +0 -0
  38. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/optimistic-lock.md +0 -0
  39. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/pagination.md +0 -0
  40. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/polymorphic.md +0 -0
  41. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/relation-load-checker.md +0 -0
  42. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/guide/relation-preload.md +0 -0
  43. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/cache-queries.md +0 -0
  44. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/configure-cascade-delete.md +0 -0
  45. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/define-jti-models.md +0 -0
  46. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/define-sti-models.md +0 -0
  47. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/handle-concurrent-updates.md +0 -0
  48. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/index.md +0 -0
  49. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/integrate-with-fastapi.md +0 -0
  50. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
  51. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
  52. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/how-to/release-connection-during-long-io.md +0 -0
  53. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/cached-table.md +0 -0
  54. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/crud.md +0 -0
  55. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/index.md +0 -0
  56. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/metaclass.md +0 -0
  57. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/optimistic-lock.md +0 -0
  58. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/polymorphic.md +0 -0
  59. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/prerequisites.md +0 -0
  60. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/relation-load-checker.md +0 -0
  61. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/internals/relation-preload.md +0 -0
  62. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/base-classes.md +0 -0
  63. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/crud-methods.md +0 -0
  64. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/decorators.md +0 -0
  65. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/index.md +0 -0
  66. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/mixins.md +0 -0
  67. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/reference/pagination-types.md +0 -0
  68. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/tutorials/02-building-a-blog-api.md +0 -0
  69. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
  70. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/cached-table.md +0 -0
  71. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/cascade-delete-semantics.md +0 -0
  72. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/crud-pipeline.md +0 -0
  73. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/index.md +0 -0
  74. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/metaclass.md +0 -0
  75. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/optimistic-lock.md +0 -0
  76. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/polymorphic-internals.md +0 -0
  77. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/prerequisites.md +0 -0
  78. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/relation-load-checker.md +0 -0
  79. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/explanation/relation-preload.md +0 -0
  80. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/cached-table.md +0 -0
  81. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/crud.md +0 -0
  82. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/field-types.md +0 -0
  83. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/index.md +0 -0
  84. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/optimistic-lock.md +0 -0
  85. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/pagination.md +0 -0
  86. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/polymorphic.md +0 -0
  87. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/relation-load-checker.md +0 -0
  88. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/guide/relation-preload.md +0 -0
  89. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/cache-queries.md +0 -0
  90. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/configure-cascade-delete.md +0 -0
  91. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/define-jti-models.md +0 -0
  92. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/define-sti-models.md +0 -0
  93. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/handle-concurrent-updates.md +0 -0
  94. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/index.md +0 -0
  95. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/integrate-with-fastapi.md +0 -0
  96. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/paginate-a-list-endpoint.md +0 -0
  97. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/prevent-missing-greenlet.md +0 -0
  98. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/how-to/release-connection-during-long-io.md +0 -0
  99. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/cached-table.md +0 -0
  100. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/crud.md +0 -0
  101. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/index.md +0 -0
  102. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/metaclass.md +0 -0
  103. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/optimistic-lock.md +0 -0
  104. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/polymorphic.md +0 -0
  105. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/prerequisites.md +0 -0
  106. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/relation-load-checker.md +0 -0
  107. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/internals/relation-preload.md +0 -0
  108. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/base-classes.md +0 -0
  109. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/crud-methods.md +0 -0
  110. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/decorators.md +0 -0
  111. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/index.md +0 -0
  112. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/mixins.md +0 -0
  113. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/reference/pagination-types.md +0 -0
  114. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/tutorials/02-building-a-blog-api.md +0 -0
  115. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/docs/tutorials/03-adding-redis-cache.md +0 -0
  116. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/examples/consumer_integration/run.py +0 -0
  117. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/package.json +0 -0
  118. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/pnpm-lock.yaml +0 -0
  119. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_compat.py +0 -0
  120. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_exceptions.py +0 -0
  121. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_sa_type.py +0 -0
  122. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_utils.py +0 -0
  123. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/__init__.py +0 -0
  124. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
  125. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
  126. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
  127. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
  128. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
  129. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -0
  130. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
  131. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
  132. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
  133. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
  134. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
  135. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
  136. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/url.py +0 -0
  137. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/__init__.py +0 -0
  138. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/cached_table.py +0 -0
  139. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/info_response.py +0 -0
  140. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
  141. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
  142. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
  143. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/table.py +0 -0
  144. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/pagination.py +0 -0
  145. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/relation_load_checker.py +0 -0
  146. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/__init__.py +0 -0
  147. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/_models.py +0 -0
  148. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/conftest.py +0 -0
  149. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/test_annotated_default_factory_inheritance.py +0 -0
  150. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/test_basic_crud.py +0 -0
  151. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/test_smoke_imports.py +0 -0
  152. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/tests/test_sti_default_isolation.py +0 -0
  153. {sqlmodel_ext-0.3.4 → sqlmodel_ext-0.3.5}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-ext
3
- Version: 0.3.4
3
+ Version: 0.3.5
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
@@ -170,10 +170,12 @@ async def demo(session: AsyncSession):
170
170
  A complete REST API -- models, DTOs, and five endpoints:
171
171
 
172
172
  ```python
173
+ from collections.abc import AsyncGenerator
173
174
  from typing import Annotated
174
175
  from uuid import UUID
175
176
 
176
177
  from fastapi import APIRouter, Depends
178
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
177
179
  from sqlmodel import Field
178
180
  from sqlmodel.ext.asyncio.session import AsyncSession
179
181
  from sqlmodel_ext import (
@@ -181,17 +183,37 @@ from sqlmodel_ext import (
181
183
  ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin,
182
184
  )
183
185
 
184
- # ── Dependencies (defined once, reused everywhere) ────────────────
186
+ # ── Dependency-injection layer: declare once (e.g. in a shared
187
+ # deps module), every router imports the type aliases ────────────
188
+
189
+ _engine = create_async_engine("postgresql+asyncpg://localhost/app")
190
+ _Session = async_sessionmaker(_engine, expire_on_commit=True)
191
+
192
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
193
+ async with _Session() as session:
194
+ yield session
185
195
 
186
196
  SessionDep = Annotated[AsyncSession, Depends(get_session)]
187
- TableViewDep = Annotated[TableViewRequest, Depends()]
197
+ """Request-scoped AsyncSession. The single way endpoints touch the DB."""
188
198
 
189
- # ── Models ────────────────────────────────────────────────────────
199
+ # TableViewRequest is a Pydantic model → FastAPI binds its fields as
200
+ # query params automatically. No hand-written offset/limit/order plumbing.
201
+ TableViewRequestDep = Annotated[TableViewRequest, Depends()]
202
+
203
+ async def get_current_user(session: SessionDep) -> "User":
204
+ ... # decode the bearer token, load the user — your auth, unchanged
205
+
206
+ CurrentUserDep = Annotated["User", Depends(get_current_user)]
207
+
208
+ # ── Models: the DTO ladder (Base → Create → Update → Response) ─────
190
209
 
191
210
  class ArticleBase(SQLModelBase):
192
211
  title: Str64
212
+ """Article title"""
193
213
  body: Text10K
214
+ """Article body (max 10k chars)"""
194
215
  is_published: bool = False
216
+ """Whether the article is publicly visible"""
195
217
 
196
218
  class Article(ArticleBase, UUIDTableBaseMixin, table=True):
197
219
  author_id: UUID = Field(foreign_key='user.id')
@@ -199,14 +221,24 @@ class Article(ArticleBase, UUIDTableBaseMixin, table=True):
199
221
  class ArticleCreate(ArticleBase):
200
222
  pass
201
223
 
202
- class ArticleUpdate(ArticleBase):
203
- title: Str64 | None = None # Override to optional,
204
- body: Text10K | None = None # preserving the original
205
- is_published: bool | None = None # type constraints from Base
224
+ # all_fields_optional flips every inherited field to ``T | None = None``
225
+ # while preserving its Annotated constraints AND attribute docstrings —
226
+ # no hand-maintained per-field overrides.
227
+ class ArticleUpdate(ArticleBase, all_fields_optional=True):
228
+ pass
206
229
 
207
230
  class ArticleResponse(ArticleBase, UUIDIdDatetimeInfoMixin):
208
231
  author_id: UUID
209
232
 
233
+ # ── Resource-as-dependency: wrap "load by id or 404" once, then the
234
+ # fetched ORM instance is just another injected parameter ────────
235
+
236
+ async def get_article(session: SessionDep, article_id: UUID) -> Article:
237
+ return await Article.get_exist_one(session, article_id)
238
+
239
+ ArticleDep = Annotated[Article, Depends(get_article)]
240
+ """The Article for {article_id}, or 404 — fetched before the handler runs."""
241
+
210
242
  # ── Endpoints ─────────────────────────────────────────────────────
211
243
 
212
244
  router = APIRouter(prefix="/articles", tags=["articles"])
@@ -220,7 +252,7 @@ async def create_article(
220
252
 
221
253
  @router.get("", response_model=ListResponse[ArticleResponse])
222
254
  async def list_articles(
223
- session: SessionDep, table_view: TableViewDep,
255
+ session: SessionDep, table_view: TableViewRequestDep,
224
256
  ) -> ListResponse[Article]:
225
257
  return await Article.get_with_count(
226
258
  session,
@@ -228,24 +260,24 @@ async def list_articles(
228
260
  table_view=table_view,
229
261
  )
230
262
 
263
+ # article: ArticleDep — the 404 + fetch is the dependency's job, so the
264
+ # single-resource handlers carry no lookup boilerplate at all.
231
265
  @router.get("/{article_id}", response_model=ArticleResponse)
232
- async def get_article(session: SessionDep, article_id: UUID) -> Article:
233
- return await Article.get_exist_one(session, article_id)
266
+ async def get_article_detail(article: ArticleDep) -> Article:
267
+ return article
234
268
 
235
269
  @router.patch("/{article_id}", response_model=ArticleResponse)
236
270
  async def update_article(
237
- session: SessionDep, article_id: UUID, data: ArticleUpdate,
271
+ session: SessionDep, article: ArticleDep, data: ArticleUpdate,
238
272
  ) -> Article:
239
- article = await Article.get_exist_one(session, article_id)
240
273
  return await article.update(session, data)
241
274
 
242
275
  @router.delete("/{article_id}")
243
- async def delete_article(session: SessionDep, article_id: UUID) -> None:
244
- article = await Article.get_exist_one(session, article_id)
276
+ async def delete_article(session: SessionDep, article: ArticleDep) -> None:
245
277
  await Article.delete(session, article)
246
278
  ```
247
279
 
248
- No manual SQL, no hand-written pagination logic, no boilerplate session management. The `TableViewDep` gives clients `offset`, `limit`, `desc`, `order`, and four time filters out of the box.
280
+ No manual SQL, no hand-written pagination logic, no boilerplate session management. The `TableViewRequestDep` gives clients `offset`, `limit`, `desc`, `order`, and four time filters out of the box.
249
281
 
250
282
  **What the client gets from `GET /articles?offset=0&limit=10&desc=true`:**
251
283
 
@@ -440,7 +472,7 @@ class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMix
440
472
 
441
473
  @router.get("/notifications", response_model=ListResponse[NotificationBase])
442
474
  async def list_notifications(
443
- session: SessionDep, user: CurrentUserDep, table_view: TableViewDep,
475
+ session: SessionDep, user: CurrentUserDep, table_view: TableViewRequestDep,
444
476
  ) -> ListResponse[Notification]:
445
477
  return await Notification.get_with_count(
446
478
  session,
@@ -125,10 +125,12 @@ async def demo(session: AsyncSession):
125
125
  A complete REST API -- models, DTOs, and five endpoints:
126
126
 
127
127
  ```python
128
+ from collections.abc import AsyncGenerator
128
129
  from typing import Annotated
129
130
  from uuid import UUID
130
131
 
131
132
  from fastapi import APIRouter, Depends
133
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
132
134
  from sqlmodel import Field
133
135
  from sqlmodel.ext.asyncio.session import AsyncSession
134
136
  from sqlmodel_ext import (
@@ -136,17 +138,37 @@ from sqlmodel_ext import (
136
138
  ListResponse, TableViewRequest, UUIDIdDatetimeInfoMixin,
137
139
  )
138
140
 
139
- # ── Dependencies (defined once, reused everywhere) ────────────────
141
+ # ── Dependency-injection layer: declare once (e.g. in a shared
142
+ # deps module), every router imports the type aliases ────────────
143
+
144
+ _engine = create_async_engine("postgresql+asyncpg://localhost/app")
145
+ _Session = async_sessionmaker(_engine, expire_on_commit=True)
146
+
147
+ async def get_session() -> AsyncGenerator[AsyncSession, None]:
148
+ async with _Session() as session:
149
+ yield session
140
150
 
141
151
  SessionDep = Annotated[AsyncSession, Depends(get_session)]
142
- TableViewDep = Annotated[TableViewRequest, Depends()]
152
+ """Request-scoped AsyncSession. The single way endpoints touch the DB."""
143
153
 
144
- # ── Models ────────────────────────────────────────────────────────
154
+ # TableViewRequest is a Pydantic model → FastAPI binds its fields as
155
+ # query params automatically. No hand-written offset/limit/order plumbing.
156
+ TableViewRequestDep = Annotated[TableViewRequest, Depends()]
157
+
158
+ async def get_current_user(session: SessionDep) -> "User":
159
+ ... # decode the bearer token, load the user — your auth, unchanged
160
+
161
+ CurrentUserDep = Annotated["User", Depends(get_current_user)]
162
+
163
+ # ── Models: the DTO ladder (Base → Create → Update → Response) ─────
145
164
 
146
165
  class ArticleBase(SQLModelBase):
147
166
  title: Str64
167
+ """Article title"""
148
168
  body: Text10K
169
+ """Article body (max 10k chars)"""
149
170
  is_published: bool = False
171
+ """Whether the article is publicly visible"""
150
172
 
151
173
  class Article(ArticleBase, UUIDTableBaseMixin, table=True):
152
174
  author_id: UUID = Field(foreign_key='user.id')
@@ -154,14 +176,24 @@ class Article(ArticleBase, UUIDTableBaseMixin, table=True):
154
176
  class ArticleCreate(ArticleBase):
155
177
  pass
156
178
 
157
- class ArticleUpdate(ArticleBase):
158
- title: Str64 | None = None # Override to optional,
159
- body: Text10K | None = None # preserving the original
160
- is_published: bool | None = None # type constraints from Base
179
+ # all_fields_optional flips every inherited field to ``T | None = None``
180
+ # while preserving its Annotated constraints AND attribute docstrings —
181
+ # no hand-maintained per-field overrides.
182
+ class ArticleUpdate(ArticleBase, all_fields_optional=True):
183
+ pass
161
184
 
162
185
  class ArticleResponse(ArticleBase, UUIDIdDatetimeInfoMixin):
163
186
  author_id: UUID
164
187
 
188
+ # ── Resource-as-dependency: wrap "load by id or 404" once, then the
189
+ # fetched ORM instance is just another injected parameter ────────
190
+
191
+ async def get_article(session: SessionDep, article_id: UUID) -> Article:
192
+ return await Article.get_exist_one(session, article_id)
193
+
194
+ ArticleDep = Annotated[Article, Depends(get_article)]
195
+ """The Article for {article_id}, or 404 — fetched before the handler runs."""
196
+
165
197
  # ── Endpoints ─────────────────────────────────────────────────────
166
198
 
167
199
  router = APIRouter(prefix="/articles", tags=["articles"])
@@ -175,7 +207,7 @@ async def create_article(
175
207
 
176
208
  @router.get("", response_model=ListResponse[ArticleResponse])
177
209
  async def list_articles(
178
- session: SessionDep, table_view: TableViewDep,
210
+ session: SessionDep, table_view: TableViewRequestDep,
179
211
  ) -> ListResponse[Article]:
180
212
  return await Article.get_with_count(
181
213
  session,
@@ -183,24 +215,24 @@ async def list_articles(
183
215
  table_view=table_view,
184
216
  )
185
217
 
218
+ # article: ArticleDep — the 404 + fetch is the dependency's job, so the
219
+ # single-resource handlers carry no lookup boilerplate at all.
186
220
  @router.get("/{article_id}", response_model=ArticleResponse)
187
- async def get_article(session: SessionDep, article_id: UUID) -> Article:
188
- return await Article.get_exist_one(session, article_id)
221
+ async def get_article_detail(article: ArticleDep) -> Article:
222
+ return article
189
223
 
190
224
  @router.patch("/{article_id}", response_model=ArticleResponse)
191
225
  async def update_article(
192
- session: SessionDep, article_id: UUID, data: ArticleUpdate,
226
+ session: SessionDep, article: ArticleDep, data: ArticleUpdate,
193
227
  ) -> Article:
194
- article = await Article.get_exist_one(session, article_id)
195
228
  return await article.update(session, data)
196
229
 
197
230
  @router.delete("/{article_id}")
198
- async def delete_article(session: SessionDep, article_id: UUID) -> None:
199
- article = await Article.get_exist_one(session, article_id)
231
+ async def delete_article(session: SessionDep, article: ArticleDep) -> None:
200
232
  await Article.delete(session, article)
201
233
  ```
202
234
 
203
- No manual SQL, no hand-written pagination logic, no boilerplate session management. The `TableViewDep` gives clients `offset`, `limit`, `desc`, `order`, and four time filters out of the box.
235
+ No manual SQL, no hand-written pagination logic, no boilerplate session management. The `TableViewRequestDep` gives clients `offset`, `limit`, `desc`, `order`, and four time filters out of the box.
204
236
 
205
237
  **What the client gets from `GET /articles?offset=0&limit=10&desc=true`:**
206
238
 
@@ -395,7 +427,7 @@ class PushNotification(NotifSubclassId, Notification, AutoPolymorphicIdentityMix
395
427
 
396
428
  @router.get("/notifications", response_model=ListResponse[NotificationBase])
397
429
  async def list_notifications(
398
- session: SessionDep, user: CurrentUserDep, table_view: TableViewDep,
430
+ session: SessionDep, user: CurrentUserDep, table_view: TableViewRequestDep,
399
431
  ) -> ListResponse[Notification]:
400
432
  return await Notification.get_with_count(
401
433
  session,
@@ -34,12 +34,3 @@ features:
34
34
  - title: Safe & Reliable
35
35
  details: Optimistic locking for concurrency control, @requires_relations to prevent MissingGreenlet, and AST static analysis at startup
36
36
  ---
37
-
38
- ## Design orientation
39
-
40
- sqlmodel-ext is not a new ORM. It is a set of **opt-in Mixins** layered on top of SQLModel / Pydantic v2 / SQLAlchemy 2.0. The library only introduces two kinds of machinery:
41
-
42
- - **Metaclass enhancements** — automatic SQLAlchemy column setup driven by `Annotated` type hints, `mapper_args` merging, on-demand `*UpdateRequest` DTO generation, attribute-docstring inheritance, and a Python 3.14 (PEP 649) compatibility patch.
43
- - **Composable Mixins** — `TableBaseMixin` (async CRUD), `PolymorphicBaseMixin` (JTI/STI), `OptimisticLockMixin` (version-column concurrency), `RelationPreloadMixin` (eager loading), `CachedTableBaseMixin` (Redis two-tier cache). Each Mixin stands alone and has no hard dependency on the others.
44
-
45
- The underlying `Session`, `select()`, query construction, and migration tooling remain native SQLAlchemy — there is no custom DSL, and the library does not take over `engine` / `session` lifecycle. In practice that means you can drop a single Mixin into an existing SQLModel/SQLAlchemy project without rewriting your data-access layer.