sqlmodel-ext 0.3.3__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.
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/PKG-INFO +49 -17
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/README.md +48 -16
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/field-types.md +218 -218
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/tutorials/01-getting-started.md +174 -174
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/tutorials/index.md +32 -32
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/field-types.md +218 -218
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/tutorials/01-getting-started.md +174 -174
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/tutorials/index.md +32 -32
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/pyproject.toml +1 -1
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/__init__.py +3 -1
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/base.py +55 -4
- sqlmodel_ext-0.3.5/tests/test_array_explicit_field_sa_type.py +116 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/test_smoke_imports.py +27 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/uv.lock +1 -1
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/.github/workflows/ci.yml +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/.gitignore +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/LICENSE +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/README_zh.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/.vitepress/config.mts +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/.vitepress/en.mts +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/.vitepress/theme/index.ts +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/.vitepress/theme/style/custom.css +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/.vitepress/zh.mts +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/metaclass.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/polymorphic-internals.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/cache-queries.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/integrate-with-fastapi.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/crud-methods.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/decorators.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/mixins.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/reference/pagination-types.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/tutorials/02-building-a-blog-api.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/en/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/cascade-delete-semantics.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/crud-pipeline.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/metaclass.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/polymorphic-internals.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/explanation/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/crud.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/field-types.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/pagination.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/guide/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/cache-queries.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/configure-cascade-delete.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/define-jti-models.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/define-sti-models.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/handle-concurrent-updates.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/integrate-with-fastapi.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/paginate-a-list-endpoint.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/prevent-missing-greenlet.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/how-to/release-connection-during-long-io.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/cached-table.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/crud.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/metaclass.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/optimistic-lock.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/polymorphic.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/prerequisites.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/relation-load-checker.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/internals/relation-preload.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/base-classes.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/crud-methods.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/decorators.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/index.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/mixins.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/reference/pagination-types.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/tutorials/02-building-a-blog-api.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/docs/tutorials/03-adding-redis-cache.md +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/examples/consumer_integration/run.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/package.json +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/pnpm-lock.yaml +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_compat.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_exceptions.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_sa_type.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/_utils.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_internal/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_internal/path.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/_ssrf.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/array.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/exceptions.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/jsonb_types.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/dialects/postgresql/numpy_vector.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/ip_address.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/mixins/module_name_mixin.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/field_types/url.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/cached_table.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/info_response.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/optimistic_lock.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/polymorphic.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/relation_preload.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/mixins/table.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/pagination.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/src/sqlmodel_ext/relation_load_checker.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/__init__.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/_models.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/conftest.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/test_annotated_default_factory_inheritance.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/test_basic_crud.py +0 -0
- {sqlmodel_ext-0.3.3 → sqlmodel_ext-0.3.5}/tests/test_sti_default_isolation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-ext
|
|
3
|
-
Version: 0.3.
|
|
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
|
-
# ──
|
|
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
|
-
|
|
197
|
+
"""Request-scoped AsyncSession. The single way endpoints touch the DB."""
|
|
188
198
|
|
|
189
|
-
#
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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:
|
|
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
|
|
233
|
-
return
|
|
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,
|
|
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,
|
|
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 `
|
|
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:
|
|
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
|
-
# ──
|
|
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
|
-
|
|
152
|
+
"""Request-scoped AsyncSession. The single way endpoints touch the DB."""
|
|
143
153
|
|
|
144
|
-
#
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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:
|
|
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
|
|
188
|
-
return
|
|
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,
|
|
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,
|
|
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 `
|
|
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:
|
|
430
|
+
session: SessionDep, user: CurrentUserDep, table_view: TableViewRequestDep,
|
|
399
431
|
) -> ListResponse[Notification]:
|
|
400
432
|
return await Notification.get_with_count(
|
|
401
433
|
session,
|