fastapi-viewsets 1.3.0__tar.gz → 1.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/PKG-INFO +54 -39
  2. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/README.md +44 -35
  3. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/tortoise_adapter.py +22 -0
  4. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/PKG-INFO +54 -39
  5. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/SOURCES.txt +1 -0
  6. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/requires.txt +7 -2
  7. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/pyproject.toml +8 -5
  8. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_adapter_methods_coverage.py +20 -33
  9. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_exception_handling.py +13 -25
  10. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_missing_coverage.py +2 -1
  11. fastapi_viewsets-1.4.0/tests/test_tortoise_lifecycle.py +100 -0
  12. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/LICENSE +0 -0
  13. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/__init__.py +0 -0
  14. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/_compat.py +0 -0
  15. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/_register.py +0 -0
  16. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/async_base.py +0 -0
  17. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/async_utils.py +0 -0
  18. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/constants.py +0 -0
  19. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/db_conf.py +0 -0
  20. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/__init__.py +0 -0
  21. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/base.py +0 -0
  22. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/factory.py +0 -0
  23. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/peewee_adapter.py +0 -0
  24. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/sqlalchemy_adapter.py +0 -0
  25. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/serializer_utils.py +0 -0
  26. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/utils.py +0 -0
  27. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/dependency_links.txt +0 -0
  28. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/top_level.txt +0 -0
  29. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/setup.cfg +0 -0
  30. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/setup.py +0 -0
  31. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_base_viewset.py +0 -0
  32. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_driver_missing.py +0 -0
  33. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_utils.py +0 -0
  34. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_backward_compatibility.py +0 -0
  35. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_base_viewset.py +0 -0
  36. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_coverage_gaps.py +0 -0
  37. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_db_conf.py +0 -0
  38. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_db_conf_extended.py +0 -0
  39. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_edge_cases.py +0 -0
  40. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_error_handling.py +0 -0
  41. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_integration.py +0 -0
  42. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_orm_adapters.py +0 -0
  43. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_orm_adapters_extended.py +0 -0
  44. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_select_prefetch_related.py +0 -0
  45. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_utils.py +0 -0
  46. {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_viewsets_with_adapters.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_viewsets
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support.
5
5
  Author: Alexander Valenchits
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/svalench/fastapi_viewsets
8
+ Project-URL: Documentation, https://svalench.github.io/fastapi_viewsets/
8
9
  Project-URL: Issues, https://github.com/svalench/fastapi_viewsets/issues
9
- Project-URL: Changelog, https://github.com/svalench/fastapi_viewsets/blob/main/RELEASE_NOTES.md
10
+ Project-URL: Changelog, https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md
10
11
  Keywords: fastapi,viewsets,crud,sqlalchemy,tortoise,peewee,pydantic
11
12
  Classifier: Programming Language :: Python :: 3
12
13
  Classifier: Programming Language :: Python :: 3.9
@@ -14,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
17
19
  Classifier: License :: OSI Approved :: MIT License
18
20
  Classifier: Operating System :: OS Independent
19
21
  Classifier: Framework :: FastAPI
@@ -30,10 +32,10 @@ Requires-Dist: python-dotenv>=0.19.0
30
32
  Provides-Extra: sqlalchemy
31
33
  Requires-Dist: SQLAlchemy>=1.4.36; extra == "sqlalchemy"
32
34
  Provides-Extra: tortoise
33
- Requires-Dist: tortoise-orm>=0.20.0; extra == "tortoise"
35
+ Requires-Dist: tortoise-orm<1.0,>=0.20.0; extra == "tortoise"
34
36
  Requires-Dist: asyncpg>=0.28.0; extra == "tortoise"
35
37
  Provides-Extra: peewee
36
- Requires-Dist: peewee>=3.17.0; extra == "peewee"
38
+ Requires-Dist: peewee<4,>=3.17.0; extra == "peewee"
37
39
  Provides-Extra: test
38
40
  Requires-Dist: pytest>=7.0.0; extra == "test"
39
41
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
@@ -45,6 +47,10 @@ Provides-Extra: lint
45
47
  Requires-Dist: ruff>=0.5; extra == "lint"
46
48
  Requires-Dist: black>=24; extra == "lint"
47
49
  Requires-Dist: mypy>=1.8; extra == "lint"
50
+ Provides-Extra: docs
51
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
52
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
53
+ Requires-Dist: pymdown-extensions>=10.7; extra == "docs"
48
54
  Dynamic: license-file
49
55
 
50
56
  # fastapi-viewsets
@@ -53,12 +59,15 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
53
59
 
54
60
  [![PyPI version](https://badge.fury.io/py/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
55
61
  [![Python versions](https://img.shields.io/pypi/pyversions/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
56
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/main/LICENSE)
57
- [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
62
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
63
+ [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
58
64
  [![codecov](https://codecov.io/gh/svalench/fastapi_viewsets/graph/badge.svg)](https://codecov.io/gh/svalench/fastapi_viewsets)
59
65
  [![Downloads/month](https://static.pepy.tech/badge/fastapi-viewsets/month)](https://pepy.tech/project/fastapi-viewsets)
60
66
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
61
67
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/svalench/fastapi_viewsets/pulls)
68
+ [![Docs](https://img.shields.io/badge/docs-svalench.github.io-blue.svg)](https://svalench.github.io/fastapi_viewsets/)
69
+
70
+ **Documentation:** [https://svalench.github.io/fastapi_viewsets/](https://svalench.github.io/fastapi_viewsets/)
62
71
 
63
72
  ## Why fastapi-viewsets
64
73
 
@@ -108,7 +117,7 @@ you need, and the URL shape for **PostgreSQL**, **MySQL**, and
108
117
  | --- | --- | --- | --- |
109
118
  | **SQLAlchemy (sync)** | `psycopg[binary]` or `psycopg2-binary` | `pymysql` or `mysqlclient` | `pyodbc` + ODBC Driver 17/18 |
110
119
  | **SQLAlchemy (async)** | `asyncpg` | `aiomysql` or `asyncmy` | `aioodbc` + ODBC Driver 17/18 |
111
- | **Tortoise ORM** | `asyncpg` (built-in) | `aiomysql` (built-in) | Not supported by Tortoise |
120
+ | **Tortoise ORM** | `asyncpg` (included in `[tortoise]` extra) | `aiomysql` (install separately) | Not supported by Tortoise |
112
121
  | **Peewee** | `psycopg2-binary` | `pymysql` or `mysqlclient` | Not supported by this adapter |
113
122
 
114
123
  > The `SQLAlchemyAdapter` auto-converts a sync URL to its async
@@ -191,8 +200,9 @@ TORTOISE_APP_LABEL=models
191
200
  ```
192
201
 
193
202
  ```python
203
+ from contextlib import asynccontextmanager
204
+
194
205
  from fastapi import FastAPI
195
- from tortoise import Tortoise
196
206
 
197
207
  from fastapi_viewsets import AsyncBaseViewset
198
208
  from fastapi_viewsets.orm.factory import ORMFactory
@@ -201,24 +211,19 @@ app = FastAPI()
201
211
  adapter = ORMFactory.get_default_adapter() # built from the env vars above
202
212
 
203
213
 
204
- @app.on_event("startup")
205
- async def _init_tortoise() -> None:
206
- """Open the Tortoise connection pool and create schema if needed.
214
+ @asynccontextmanager
215
+ async def lifespan(app: FastAPI):
216
+ """Open the Tortoise connection pool at startup.
207
217
 
208
- The adapter also initializes Tortoise lazily on first DB call;
209
- doing it here gives you control over schema creation.
218
+ The adapter also initialises Tortoise lazily on the first DB call;
219
+ calling ``initialize()`` here gives you control over schema creation
220
+ and avoids a cold-start penalty on the first request.
210
221
  """
211
- await Tortoise.init(
212
- db_url=adapter.database_url,
213
- modules={adapter.app_label: adapter.models},
214
- )
215
- await Tortoise.generate_schemas(safe=True)
216
-
222
+ await adapter.initialize(generate_schemas=True)
223
+ yield
224
+ await adapter.close()
217
225
 
218
- @app.on_event("shutdown")
219
- async def _close_tortoise() -> None:
220
- """Close the Tortoise connection pool."""
221
- await Tortoise.close_connections()
226
+ app = FastAPI(lifespan=lifespan)
222
227
 
223
228
 
224
229
  # Define your Tortoise models in app/models.py and pass them to AsyncBaseViewset.
@@ -236,6 +241,10 @@ async def _close_tortoise() -> None:
236
241
  # app.include_router(items)
237
242
  ```
238
243
 
244
+ > **Note:** `generate_schemas=True` is convenient for development. In
245
+ > production, use [Aerich](https://github.com/tortoise/aerich) or another
246
+ > migration tool instead of auto-generating schemas at startup.
247
+ >
239
248
  > **MSSQL is not supported by Tortoise ORM.** Use SQLAlchemy with
240
249
  > `aioodbc` for SQL Server.
241
250
 
@@ -314,6 +323,7 @@ Save as `main.py` in an empty folder and run `python main.py` or `uvicorn main:a
314
323
  from fastapi import FastAPI
315
324
  from pydantic import BaseModel, ConfigDict
316
325
  from sqlalchemy import Column, Integer, String
326
+ from typing import Optional
317
327
 
318
328
  from fastapi_viewsets import BaseViewset
319
329
  from fastapi_viewsets.db_conf import Base, engine, get_session
@@ -333,7 +343,7 @@ class ItemSchema(BaseModel):
333
343
  """Pydantic model for request and response bodies."""
334
344
 
335
345
  model_config = ConfigDict(from_attributes=True)
336
- id: int | None = None
346
+ id: Optional[int] = None
337
347
  name: str
338
348
 
339
349
 
@@ -423,9 +433,12 @@ package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
423
433
  `postgresql://` to `postgresql+asyncpg://`, etc.
424
434
 
425
435
  ```python
436
+ from contextlib import asynccontextmanager
437
+
426
438
  from fastapi import FastAPI
427
439
  from pydantic import BaseModel, ConfigDict
428
440
  from sqlalchemy import Column, Integer, String
441
+ from typing import Optional
429
442
 
430
443
  from fastapi_viewsets import AsyncBaseViewset
431
444
  from fastapi_viewsets.db_conf import (
@@ -434,7 +447,16 @@ from fastapi_viewsets.db_conf import (
434
447
  get_async_session,
435
448
  )
436
449
 
437
- app = FastAPI()
450
+
451
+ @asynccontextmanager
452
+ async def lifespan(app: FastAPI):
453
+ """Create tables once on startup using the async engine."""
454
+ async with async_engine.begin() as conn:
455
+ await conn.run_sync(Base.metadata.create_all)
456
+ yield
457
+
458
+
459
+ app = FastAPI(lifespan=lifespan)
438
460
 
439
461
 
440
462
  class Item(Base):
@@ -449,17 +471,10 @@ class ItemSchema(BaseModel):
449
471
  """Pydantic v2 schema reused as request and response model."""
450
472
 
451
473
  model_config = ConfigDict(from_attributes=True)
452
- id: int | None = None
474
+ id: Optional[int] = None
453
475
  name: str
454
476
 
455
477
 
456
- @app.on_event("startup")
457
- async def _create_tables() -> None:
458
- """Create tables once on startup using the async engine."""
459
- async with async_engine.begin() as conn:
460
- await conn.run_sync(Base.metadata.create_all)
461
-
462
-
463
478
  items = AsyncBaseViewset(
464
479
  endpoint="/items",
465
480
  model=Item,
@@ -624,6 +639,7 @@ from fastapi import FastAPI
624
639
  from fastapi.security import OAuth2PasswordBearer
625
640
  from pydantic import BaseModel, ConfigDict
626
641
  from sqlalchemy import Column, Integer, String
642
+ from typing import Optional
627
643
  from fastapi_viewsets import BaseViewset
628
644
  from fastapi_viewsets.db_conf import Base, engine, get_session
629
645
 
@@ -642,7 +658,7 @@ class ItemSchema(BaseModel):
642
658
  """Pydantic schema for Item payloads and responses."""
643
659
 
644
660
  model_config = ConfigDict(from_attributes=True)
645
- id: int | None = None
661
+ id: Optional[int] = None
646
662
  name: str
647
663
 
648
664
 
@@ -739,11 +755,10 @@ Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.
739
755
 
740
756
  | Item | Target | Status |
741
757
  | --- | --- | --- |
742
- | Dedicated `AsyncModelViewSet` ergonomics on top of SQLAlchemy 2.x async sessions | v1.2 | Planned |
743
- | First-class Tortoise ORM viewset examples and docs (`TortoiseModelViewSet` naming TBD) | v1.2 | Planned |
744
- | Async pagination helpers and transaction boundaries across adapters | v1.3 | Planned |
745
- | Nested Pydantic models with automatic eager-loading | v1.3 | **Done** |
746
- | Wire `search` on LIST to real database queries | v1.2 | Planned |
758
+ | Wire `search` on LIST to real database queries | v1.4 | Planned |
759
+ | Transaction helpers (`begin` / `atomic`) across adapters | v1.4 | Planned |
760
+ | Declarative ordering (`order_by`) on LIST endpoints | v1.4 | Planned |
761
+ | Advanced filters (`__gt`, `__lt`, `__in`) via query params | v1.5 | Planned |
747
762
 
748
763
  ## Comparison with alternatives
749
764
 
@@ -4,12 +4,15 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
4
4
 
5
5
  [![PyPI version](https://badge.fury.io/py/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
6
6
  [![Python versions](https://img.shields.io/pypi/pyversions/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
7
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/main/LICENSE)
8
- [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
8
+ [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
9
9
  [![codecov](https://codecov.io/gh/svalench/fastapi_viewsets/graph/badge.svg)](https://codecov.io/gh/svalench/fastapi_viewsets)
10
10
  [![Downloads/month](https://static.pepy.tech/badge/fastapi-viewsets/month)](https://pepy.tech/project/fastapi-viewsets)
11
11
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
12
12
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/svalench/fastapi_viewsets/pulls)
13
+ [![Docs](https://img.shields.io/badge/docs-svalench.github.io-blue.svg)](https://svalench.github.io/fastapi_viewsets/)
14
+
15
+ **Documentation:** [https://svalench.github.io/fastapi_viewsets/](https://svalench.github.io/fastapi_viewsets/)
13
16
 
14
17
  ## Why fastapi-viewsets
15
18
 
@@ -59,7 +62,7 @@ you need, and the URL shape for **PostgreSQL**, **MySQL**, and
59
62
  | --- | --- | --- | --- |
60
63
  | **SQLAlchemy (sync)** | `psycopg[binary]` or `psycopg2-binary` | `pymysql` or `mysqlclient` | `pyodbc` + ODBC Driver 17/18 |
61
64
  | **SQLAlchemy (async)** | `asyncpg` | `aiomysql` or `asyncmy` | `aioodbc` + ODBC Driver 17/18 |
62
- | **Tortoise ORM** | `asyncpg` (built-in) | `aiomysql` (built-in) | Not supported by Tortoise |
65
+ | **Tortoise ORM** | `asyncpg` (included in `[tortoise]` extra) | `aiomysql` (install separately) | Not supported by Tortoise |
63
66
  | **Peewee** | `psycopg2-binary` | `pymysql` or `mysqlclient` | Not supported by this adapter |
64
67
 
65
68
  > The `SQLAlchemyAdapter` auto-converts a sync URL to its async
@@ -142,8 +145,9 @@ TORTOISE_APP_LABEL=models
142
145
  ```
143
146
 
144
147
  ```python
148
+ from contextlib import asynccontextmanager
149
+
145
150
  from fastapi import FastAPI
146
- from tortoise import Tortoise
147
151
 
148
152
  from fastapi_viewsets import AsyncBaseViewset
149
153
  from fastapi_viewsets.orm.factory import ORMFactory
@@ -152,24 +156,19 @@ app = FastAPI()
152
156
  adapter = ORMFactory.get_default_adapter() # built from the env vars above
153
157
 
154
158
 
155
- @app.on_event("startup")
156
- async def _init_tortoise() -> None:
157
- """Open the Tortoise connection pool and create schema if needed.
159
+ @asynccontextmanager
160
+ async def lifespan(app: FastAPI):
161
+ """Open the Tortoise connection pool at startup.
158
162
 
159
- The adapter also initializes Tortoise lazily on first DB call;
160
- doing it here gives you control over schema creation.
163
+ The adapter also initialises Tortoise lazily on the first DB call;
164
+ calling ``initialize()`` here gives you control over schema creation
165
+ and avoids a cold-start penalty on the first request.
161
166
  """
162
- await Tortoise.init(
163
- db_url=adapter.database_url,
164
- modules={adapter.app_label: adapter.models},
165
- )
166
- await Tortoise.generate_schemas(safe=True)
167
-
167
+ await adapter.initialize(generate_schemas=True)
168
+ yield
169
+ await adapter.close()
168
170
 
169
- @app.on_event("shutdown")
170
- async def _close_tortoise() -> None:
171
- """Close the Tortoise connection pool."""
172
- await Tortoise.close_connections()
171
+ app = FastAPI(lifespan=lifespan)
173
172
 
174
173
 
175
174
  # Define your Tortoise models in app/models.py and pass them to AsyncBaseViewset.
@@ -187,6 +186,10 @@ async def _close_tortoise() -> None:
187
186
  # app.include_router(items)
188
187
  ```
189
188
 
189
+ > **Note:** `generate_schemas=True` is convenient for development. In
190
+ > production, use [Aerich](https://github.com/tortoise/aerich) or another
191
+ > migration tool instead of auto-generating schemas at startup.
192
+ >
190
193
  > **MSSQL is not supported by Tortoise ORM.** Use SQLAlchemy with
191
194
  > `aioodbc` for SQL Server.
192
195
 
@@ -265,6 +268,7 @@ Save as `main.py` in an empty folder and run `python main.py` or `uvicorn main:a
265
268
  from fastapi import FastAPI
266
269
  from pydantic import BaseModel, ConfigDict
267
270
  from sqlalchemy import Column, Integer, String
271
+ from typing import Optional
268
272
 
269
273
  from fastapi_viewsets import BaseViewset
270
274
  from fastapi_viewsets.db_conf import Base, engine, get_session
@@ -284,7 +288,7 @@ class ItemSchema(BaseModel):
284
288
  """Pydantic model for request and response bodies."""
285
289
 
286
290
  model_config = ConfigDict(from_attributes=True)
287
- id: int | None = None
291
+ id: Optional[int] = None
288
292
  name: str
289
293
 
290
294
 
@@ -374,9 +378,12 @@ package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
374
378
  `postgresql://` to `postgresql+asyncpg://`, etc.
375
379
 
376
380
  ```python
381
+ from contextlib import asynccontextmanager
382
+
377
383
  from fastapi import FastAPI
378
384
  from pydantic import BaseModel, ConfigDict
379
385
  from sqlalchemy import Column, Integer, String
386
+ from typing import Optional
380
387
 
381
388
  from fastapi_viewsets import AsyncBaseViewset
382
389
  from fastapi_viewsets.db_conf import (
@@ -385,7 +392,16 @@ from fastapi_viewsets.db_conf import (
385
392
  get_async_session,
386
393
  )
387
394
 
388
- app = FastAPI()
395
+
396
+ @asynccontextmanager
397
+ async def lifespan(app: FastAPI):
398
+ """Create tables once on startup using the async engine."""
399
+ async with async_engine.begin() as conn:
400
+ await conn.run_sync(Base.metadata.create_all)
401
+ yield
402
+
403
+
404
+ app = FastAPI(lifespan=lifespan)
389
405
 
390
406
 
391
407
  class Item(Base):
@@ -400,17 +416,10 @@ class ItemSchema(BaseModel):
400
416
  """Pydantic v2 schema reused as request and response model."""
401
417
 
402
418
  model_config = ConfigDict(from_attributes=True)
403
- id: int | None = None
419
+ id: Optional[int] = None
404
420
  name: str
405
421
 
406
422
 
407
- @app.on_event("startup")
408
- async def _create_tables() -> None:
409
- """Create tables once on startup using the async engine."""
410
- async with async_engine.begin() as conn:
411
- await conn.run_sync(Base.metadata.create_all)
412
-
413
-
414
423
  items = AsyncBaseViewset(
415
424
  endpoint="/items",
416
425
  model=Item,
@@ -575,6 +584,7 @@ from fastapi import FastAPI
575
584
  from fastapi.security import OAuth2PasswordBearer
576
585
  from pydantic import BaseModel, ConfigDict
577
586
  from sqlalchemy import Column, Integer, String
587
+ from typing import Optional
578
588
  from fastapi_viewsets import BaseViewset
579
589
  from fastapi_viewsets.db_conf import Base, engine, get_session
580
590
 
@@ -593,7 +603,7 @@ class ItemSchema(BaseModel):
593
603
  """Pydantic schema for Item payloads and responses."""
594
604
 
595
605
  model_config = ConfigDict(from_attributes=True)
596
- id: int | None = None
606
+ id: Optional[int] = None
597
607
  name: str
598
608
 
599
609
 
@@ -690,11 +700,10 @@ Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.
690
700
 
691
701
  | Item | Target | Status |
692
702
  | --- | --- | --- |
693
- | Dedicated `AsyncModelViewSet` ergonomics on top of SQLAlchemy 2.x async sessions | v1.2 | Planned |
694
- | First-class Tortoise ORM viewset examples and docs (`TortoiseModelViewSet` naming TBD) | v1.2 | Planned |
695
- | Async pagination helpers and transaction boundaries across adapters | v1.3 | Planned |
696
- | Nested Pydantic models with automatic eager-loading | v1.3 | **Done** |
697
- | Wire `search` on LIST to real database queries | v1.2 | Planned |
703
+ | Wire `search` on LIST to real database queries | v1.4 | Planned |
704
+ | Transaction helpers (`begin` / `atomic`) across adapters | v1.4 | Planned |
705
+ | Declarative ordering (`order_by`) on LIST endpoints | v1.4 | Planned |
706
+ | Advanced filters (`__gt`, `__lt`, `__in`) via query params | v1.5 | Planned |
698
707
 
699
708
  ## Comparison with alternatives
700
709
 
@@ -58,6 +58,28 @@ class TortoiseAdapter(BaseORMAdapter):
58
58
  }
59
59
  await Tortoise.init(config=db_config)
60
60
  self._initialized = True
61
+
62
+ async def initialize(self, generate_schemas: bool = False) -> None:
63
+ """Initialize the Tortoise connection pool.
64
+
65
+ Call this in your application lifespan to explicitly open the
66
+ connection at startup instead of relying on lazy initialisation.
67
+
68
+ Args:
69
+ generate_schemas: If True, call ``Tortoise.generate_schemas(safe=True)``
70
+ after init. Useful for development; use Aerich in production.
71
+ """
72
+ await self._ensure_initialized()
73
+ if generate_schemas:
74
+ await Tortoise.generate_schemas(safe=True)
75
+
76
+ async def close(self) -> None:
77
+ """Close all Tortoise connections.
78
+
79
+ Call this in your application lifespan shutdown handler.
80
+ """
81
+ await Tortoise.close_connections()
82
+ self._initialized = False
61
83
 
62
84
  def get_session(self):
63
85
  """Get synchronous database session.
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_viewsets
3
- Version: 1.3.0
3
+ Version: 1.4.0
4
4
  Summary: DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support.
5
5
  Author: Alexander Valenchits
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/svalench/fastapi_viewsets
8
+ Project-URL: Documentation, https://svalench.github.io/fastapi_viewsets/
8
9
  Project-URL: Issues, https://github.com/svalench/fastapi_viewsets/issues
9
- Project-URL: Changelog, https://github.com/svalench/fastapi_viewsets/blob/main/RELEASE_NOTES.md
10
+ Project-URL: Changelog, https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md
10
11
  Keywords: fastapi,viewsets,crud,sqlalchemy,tortoise,peewee,pydantic
11
12
  Classifier: Programming Language :: Python :: 3
12
13
  Classifier: Programming Language :: Python :: 3.9
@@ -14,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
14
15
  Classifier: Programming Language :: Python :: 3.11
15
16
  Classifier: Programming Language :: Python :: 3.12
16
17
  Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
17
19
  Classifier: License :: OSI Approved :: MIT License
18
20
  Classifier: Operating System :: OS Independent
19
21
  Classifier: Framework :: FastAPI
@@ -30,10 +32,10 @@ Requires-Dist: python-dotenv>=0.19.0
30
32
  Provides-Extra: sqlalchemy
31
33
  Requires-Dist: SQLAlchemy>=1.4.36; extra == "sqlalchemy"
32
34
  Provides-Extra: tortoise
33
- Requires-Dist: tortoise-orm>=0.20.0; extra == "tortoise"
35
+ Requires-Dist: tortoise-orm<1.0,>=0.20.0; extra == "tortoise"
34
36
  Requires-Dist: asyncpg>=0.28.0; extra == "tortoise"
35
37
  Provides-Extra: peewee
36
- Requires-Dist: peewee>=3.17.0; extra == "peewee"
38
+ Requires-Dist: peewee<4,>=3.17.0; extra == "peewee"
37
39
  Provides-Extra: test
38
40
  Requires-Dist: pytest>=7.0.0; extra == "test"
39
41
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
@@ -45,6 +47,10 @@ Provides-Extra: lint
45
47
  Requires-Dist: ruff>=0.5; extra == "lint"
46
48
  Requires-Dist: black>=24; extra == "lint"
47
49
  Requires-Dist: mypy>=1.8; extra == "lint"
50
+ Provides-Extra: docs
51
+ Requires-Dist: mkdocs>=1.6; extra == "docs"
52
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
53
+ Requires-Dist: pymdown-extensions>=10.7; extra == "docs"
48
54
  Dynamic: license-file
49
55
 
50
56
  # fastapi-viewsets
@@ -53,12 +59,15 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
53
59
 
54
60
  [![PyPI version](https://badge.fury.io/py/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
55
61
  [![Python versions](https://img.shields.io/pypi/pyversions/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
56
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/main/LICENSE)
57
- [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
62
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
63
+ [![CI](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
58
64
  [![codecov](https://codecov.io/gh/svalench/fastapi_viewsets/graph/badge.svg)](https://codecov.io/gh/svalench/fastapi_viewsets)
59
65
  [![Downloads/month](https://static.pepy.tech/badge/fastapi-viewsets/month)](https://pepy.tech/project/fastapi-viewsets)
60
66
  [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
61
67
  [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/svalench/fastapi_viewsets/pulls)
68
+ [![Docs](https://img.shields.io/badge/docs-svalench.github.io-blue.svg)](https://svalench.github.io/fastapi_viewsets/)
69
+
70
+ **Documentation:** [https://svalench.github.io/fastapi_viewsets/](https://svalench.github.io/fastapi_viewsets/)
62
71
 
63
72
  ## Why fastapi-viewsets
64
73
 
@@ -108,7 +117,7 @@ you need, and the URL shape for **PostgreSQL**, **MySQL**, and
108
117
  | --- | --- | --- | --- |
109
118
  | **SQLAlchemy (sync)** | `psycopg[binary]` or `psycopg2-binary` | `pymysql` or `mysqlclient` | `pyodbc` + ODBC Driver 17/18 |
110
119
  | **SQLAlchemy (async)** | `asyncpg` | `aiomysql` or `asyncmy` | `aioodbc` + ODBC Driver 17/18 |
111
- | **Tortoise ORM** | `asyncpg` (built-in) | `aiomysql` (built-in) | Not supported by Tortoise |
120
+ | **Tortoise ORM** | `asyncpg` (included in `[tortoise]` extra) | `aiomysql` (install separately) | Not supported by Tortoise |
112
121
  | **Peewee** | `psycopg2-binary` | `pymysql` or `mysqlclient` | Not supported by this adapter |
113
122
 
114
123
  > The `SQLAlchemyAdapter` auto-converts a sync URL to its async
@@ -191,8 +200,9 @@ TORTOISE_APP_LABEL=models
191
200
  ```
192
201
 
193
202
  ```python
203
+ from contextlib import asynccontextmanager
204
+
194
205
  from fastapi import FastAPI
195
- from tortoise import Tortoise
196
206
 
197
207
  from fastapi_viewsets import AsyncBaseViewset
198
208
  from fastapi_viewsets.orm.factory import ORMFactory
@@ -201,24 +211,19 @@ app = FastAPI()
201
211
  adapter = ORMFactory.get_default_adapter() # built from the env vars above
202
212
 
203
213
 
204
- @app.on_event("startup")
205
- async def _init_tortoise() -> None:
206
- """Open the Tortoise connection pool and create schema if needed.
214
+ @asynccontextmanager
215
+ async def lifespan(app: FastAPI):
216
+ """Open the Tortoise connection pool at startup.
207
217
 
208
- The adapter also initializes Tortoise lazily on first DB call;
209
- doing it here gives you control over schema creation.
218
+ The adapter also initialises Tortoise lazily on the first DB call;
219
+ calling ``initialize()`` here gives you control over schema creation
220
+ and avoids a cold-start penalty on the first request.
210
221
  """
211
- await Tortoise.init(
212
- db_url=adapter.database_url,
213
- modules={adapter.app_label: adapter.models},
214
- )
215
- await Tortoise.generate_schemas(safe=True)
216
-
222
+ await adapter.initialize(generate_schemas=True)
223
+ yield
224
+ await adapter.close()
217
225
 
218
- @app.on_event("shutdown")
219
- async def _close_tortoise() -> None:
220
- """Close the Tortoise connection pool."""
221
- await Tortoise.close_connections()
226
+ app = FastAPI(lifespan=lifespan)
222
227
 
223
228
 
224
229
  # Define your Tortoise models in app/models.py and pass them to AsyncBaseViewset.
@@ -236,6 +241,10 @@ async def _close_tortoise() -> None:
236
241
  # app.include_router(items)
237
242
  ```
238
243
 
244
+ > **Note:** `generate_schemas=True` is convenient for development. In
245
+ > production, use [Aerich](https://github.com/tortoise/aerich) or another
246
+ > migration tool instead of auto-generating schemas at startup.
247
+ >
239
248
  > **MSSQL is not supported by Tortoise ORM.** Use SQLAlchemy with
240
249
  > `aioodbc` for SQL Server.
241
250
 
@@ -314,6 +323,7 @@ Save as `main.py` in an empty folder and run `python main.py` or `uvicorn main:a
314
323
  from fastapi import FastAPI
315
324
  from pydantic import BaseModel, ConfigDict
316
325
  from sqlalchemy import Column, Integer, String
326
+ from typing import Optional
317
327
 
318
328
  from fastapi_viewsets import BaseViewset
319
329
  from fastapi_viewsets.db_conf import Base, engine, get_session
@@ -333,7 +343,7 @@ class ItemSchema(BaseModel):
333
343
  """Pydantic model for request and response bodies."""
334
344
 
335
345
  model_config = ConfigDict(from_attributes=True)
336
- id: int | None = None
346
+ id: Optional[int] = None
337
347
  name: str
338
348
 
339
349
 
@@ -423,9 +433,12 @@ package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
423
433
  `postgresql://` to `postgresql+asyncpg://`, etc.
424
434
 
425
435
  ```python
436
+ from contextlib import asynccontextmanager
437
+
426
438
  from fastapi import FastAPI
427
439
  from pydantic import BaseModel, ConfigDict
428
440
  from sqlalchemy import Column, Integer, String
441
+ from typing import Optional
429
442
 
430
443
  from fastapi_viewsets import AsyncBaseViewset
431
444
  from fastapi_viewsets.db_conf import (
@@ -434,7 +447,16 @@ from fastapi_viewsets.db_conf import (
434
447
  get_async_session,
435
448
  )
436
449
 
437
- app = FastAPI()
450
+
451
+ @asynccontextmanager
452
+ async def lifespan(app: FastAPI):
453
+ """Create tables once on startup using the async engine."""
454
+ async with async_engine.begin() as conn:
455
+ await conn.run_sync(Base.metadata.create_all)
456
+ yield
457
+
458
+
459
+ app = FastAPI(lifespan=lifespan)
438
460
 
439
461
 
440
462
  class Item(Base):
@@ -449,17 +471,10 @@ class ItemSchema(BaseModel):
449
471
  """Pydantic v2 schema reused as request and response model."""
450
472
 
451
473
  model_config = ConfigDict(from_attributes=True)
452
- id: int | None = None
474
+ id: Optional[int] = None
453
475
  name: str
454
476
 
455
477
 
456
- @app.on_event("startup")
457
- async def _create_tables() -> None:
458
- """Create tables once on startup using the async engine."""
459
- async with async_engine.begin() as conn:
460
- await conn.run_sync(Base.metadata.create_all)
461
-
462
-
463
478
  items = AsyncBaseViewset(
464
479
  endpoint="/items",
465
480
  model=Item,
@@ -624,6 +639,7 @@ from fastapi import FastAPI
624
639
  from fastapi.security import OAuth2PasswordBearer
625
640
  from pydantic import BaseModel, ConfigDict
626
641
  from sqlalchemy import Column, Integer, String
642
+ from typing import Optional
627
643
  from fastapi_viewsets import BaseViewset
628
644
  from fastapi_viewsets.db_conf import Base, engine, get_session
629
645
 
@@ -642,7 +658,7 @@ class ItemSchema(BaseModel):
642
658
  """Pydantic schema for Item payloads and responses."""
643
659
 
644
660
  model_config = ConfigDict(from_attributes=True)
645
- id: int | None = None
661
+ id: Optional[int] = None
646
662
  name: str
647
663
 
648
664
 
@@ -739,11 +755,10 @@ Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.
739
755
 
740
756
  | Item | Target | Status |
741
757
  | --- | --- | --- |
742
- | Dedicated `AsyncModelViewSet` ergonomics on top of SQLAlchemy 2.x async sessions | v1.2 | Planned |
743
- | First-class Tortoise ORM viewset examples and docs (`TortoiseModelViewSet` naming TBD) | v1.2 | Planned |
744
- | Async pagination helpers and transaction boundaries across adapters | v1.3 | Planned |
745
- | Nested Pydantic models with automatic eager-loading | v1.3 | **Done** |
746
- | Wire `search` on LIST to real database queries | v1.2 | Planned |
758
+ | Wire `search` on LIST to real database queries | v1.4 | Planned |
759
+ | Transaction helpers (`begin` / `atomic`) across adapters | v1.4 | Planned |
760
+ | Declarative ordering (`order_by`) on LIST endpoints | v1.4 | Planned |
761
+ | Advanced filters (`__gt`, `__lt`, `__in`) via query params | v1.5 | Planned |
747
762
 
748
763
  ## Comparison with alternatives
749
764
 
@@ -40,5 +40,6 @@ tests/test_missing_coverage.py
40
40
  tests/test_orm_adapters.py
41
41
  tests/test_orm_adapters_extended.py
42
42
  tests/test_select_prefetch_related.py
43
+ tests/test_tortoise_lifecycle.py
43
44
  tests/test_utils.py
44
45
  tests/test_viewsets_with_adapters.py
@@ -4,13 +4,18 @@ SQLAlchemy>=1.4.36
4
4
  pydantic<3,>=2.5
5
5
  python-dotenv>=0.19.0
6
6
 
7
+ [docs]
8
+ mkdocs>=1.6
9
+ mkdocs-material>=9.5
10
+ pymdown-extensions>=10.7
11
+
7
12
  [lint]
8
13
  ruff>=0.5
9
14
  black>=24
10
15
  mypy>=1.8
11
16
 
12
17
  [peewee]
13
- peewee>=3.17.0
18
+ peewee<4,>=3.17.0
14
19
 
15
20
  [sqlalchemy]
16
21
  SQLAlchemy>=1.4.36
@@ -24,5 +29,5 @@ faker>=18.0.0
24
29
  aiosqlite>=0.19.0
25
30
 
26
31
  [tortoise]
27
- tortoise-orm>=0.20.0
32
+ tortoise-orm<1.0,>=0.20.0
28
33
  asyncpg>=0.28.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "fastapi_viewsets"
7
- version = "1.3.0"
7
+ version = "1.4.0"
8
8
  description = "DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -18,6 +18,7 @@ classifiers = [
18
18
  "Programming Language :: Python :: 3.11",
19
19
  "Programming Language :: Python :: 3.12",
20
20
  "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
21
22
  "License :: OSI Approved :: MIT License",
22
23
  "Operating System :: OS Independent",
23
24
  "Framework :: FastAPI",
@@ -34,8 +35,8 @@ dependencies = [
34
35
 
35
36
  [project.optional-dependencies]
36
37
  sqlalchemy = ["SQLAlchemy>=1.4.36"]
37
- tortoise = ["tortoise-orm>=0.20.0", "asyncpg>=0.28.0"]
38
- peewee = ["peewee>=3.17.0"]
38
+ tortoise = ["tortoise-orm>=0.20.0,<1.0", "asyncpg>=0.28.0"]
39
+ peewee = ["peewee>=3.17.0,<4"]
39
40
  test = [
40
41
  "pytest>=7.0.0",
41
42
  "pytest-asyncio>=0.21.0",
@@ -45,11 +46,13 @@ test = [
45
46
  "aiosqlite>=0.19.0",
46
47
  ]
47
48
  lint = ["ruff>=0.5", "black>=24", "mypy>=1.8"]
49
+ docs = ["mkdocs>=1.6", "mkdocs-material>=9.5", "pymdown-extensions>=10.7"]
48
50
 
49
51
  [project.urls]
50
52
  Homepage = "https://github.com/svalench/fastapi_viewsets"
53
+ Documentation = "https://svalench.github.io/fastapi_viewsets/"
51
54
  Issues = "https://github.com/svalench/fastapi_viewsets/issues"
52
- Changelog = "https://github.com/svalench/fastapi_viewsets/blob/main/RELEASE_NOTES.md"
55
+ Changelog = "https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md"
53
56
 
54
57
  [tool.setuptools.packages.find]
55
58
  where = ["."]
@@ -58,7 +61,7 @@ exclude = ["tests*"]
58
61
 
59
62
  [tool.black]
60
63
  line-length = 100
61
- target-version = ["py39", "py310", "py311", "py312", "py313"]
64
+ target-version = ["py39", "py310", "py311", "py312", "py313", "py314"]
62
65
 
63
66
  [tool.ruff]
64
67
  line-length = 100
@@ -186,21 +186,18 @@ class TestTortoiseAdapterMethods:
186
186
  """Test Tortoise adapter get_list_queryset_async with offset."""
187
187
  try:
188
188
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
189
- from tortoise.models import Model
190
- from tortoise import fields
191
-
189
+ from tests.tortoise_models import SimpleTortoiseModel as TestModel
190
+
192
191
  adapter = TortoiseAdapter(
193
- database_url="sqlite:///test_tortoise_offset.db",
194
- models=[],
192
+ database_url="sqlite://:memory:",
193
+ models=["tests.tortoise_models"],
195
194
  app_label="test"
196
195
  )
197
-
198
- class TestModel(Model):
199
- id = fields.IntField(pk=True)
200
- name = fields.CharField(max_length=50)
201
-
202
- # Initialize
196
+
197
+ # Initialize and create the table
198
+ from tortoise import Tortoise
203
199
  await adapter._ensure_initialized()
200
+ await Tortoise.generate_schemas(safe=True)
204
201
 
205
202
  # Create test data
206
203
  await TestModel.create(name="test1")
@@ -228,20 +225,14 @@ class TestTortoiseAdapterMethods:
228
225
  """Test Tortoise adapter create_element_async validation."""
229
226
  try:
230
227
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
231
- from tortoise.models import Model
232
- from tortoise import fields
233
-
228
+ from tests.tortoise_models import SimpleTortoiseModel as TestModel
229
+
234
230
  adapter = TortoiseAdapter(
235
- database_url="sqlite:///test_tortoise_validation.db",
236
- models=[],
231
+ database_url="sqlite://:memory:",
232
+ models=["tests.tortoise_models"],
237
233
  app_label="test"
238
234
  )
239
-
240
- class TestModel(Model):
241
- id = fields.IntField(pk=True)
242
- name = fields.CharField(max_length=50, required=True)
243
- value = fields.IntField(null=True)
244
-
235
+
245
236
  await adapter._ensure_initialized()
246
237
 
247
238
  # Test missing required field
@@ -266,21 +257,17 @@ class TestTortoiseAdapterMethods:
266
257
  """Test Tortoise adapter update_element_async with PUT."""
267
258
  try:
268
259
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
269
- from tortoise.models import Model
270
- from tortoise import fields
271
-
260
+ from tests.tortoise_models import SimpleTortoiseModel as TestModel
261
+
272
262
  adapter = TortoiseAdapter(
273
- database_url="sqlite:///test_tortoise_put.db",
274
- models=[],
263
+ database_url="sqlite://:memory:",
264
+ models=["tests.tortoise_models"],
275
265
  app_label="test"
276
266
  )
277
-
278
- class TestModel(Model):
279
- id = fields.IntField(pk=True)
280
- name = fields.CharField(max_length=50)
281
- value = fields.IntField(null=True)
282
-
267
+
268
+ from tortoise import Tortoise
283
269
  await adapter._ensure_initialized()
270
+ await Tortoise.generate_schemas(safe=True)
284
271
 
285
272
  # Create test data
286
273
  obj = await TestModel.create(name="old", value=10)
@@ -280,21 +280,17 @@ class TestTortoiseAdapterExceptions:
280
280
  """Test Tortoise adapter create_element_async handles IntegrityError."""
281
281
  try:
282
282
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
283
- from tortoise.models import Model
284
- from tortoise import fields
285
- from tortoise.exceptions import IntegrityError as TortoiseIntegrityError
283
+ from tests.tortoise_models import UniqueNameTortoiseModel as TestModel
286
284
 
287
285
  adapter = TortoiseAdapter(
288
- database_url="sqlite:///test_tortoise_integrity.db",
289
- models=[],
286
+ database_url="sqlite://:memory:",
287
+ models=["tests.tortoise_models"],
290
288
  app_label="test"
291
289
  )
292
290
 
293
- class TestModel(Model):
294
- id = fields.IntField(pk=True)
295
- name = fields.CharField(max_length=50, unique=True)
296
-
291
+ from tortoise import Tortoise
297
292
  await adapter._ensure_initialized()
293
+ await Tortoise.generate_schemas(safe=True)
298
294
 
299
295
  # Create first record
300
296
  await TestModel.create(name="test1")
@@ -322,19 +318,14 @@ class TestTortoiseAdapterExceptions:
322
318
  """Test Tortoise adapter create_element_async handles generic Exception."""
323
319
  try:
324
320
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
325
- from tortoise.models import Model
326
- from tortoise import fields
321
+ from tests.tortoise_models import SimpleTortoiseModel as TestModel
327
322
 
328
323
  adapter = TortoiseAdapter(
329
- database_url="sqlite:///test_tortoise_generic.db",
330
- models=[],
324
+ database_url="sqlite://:memory:",
325
+ models=["tests.tortoise_models"],
331
326
  app_label="test"
332
327
  )
333
328
 
334
- class TestModel(Model):
335
- id = fields.IntField(pk=True)
336
- name = fields.CharField(max_length=50)
337
-
338
329
  await adapter._ensure_initialized()
339
330
 
340
331
  # Mock create to raise generic exception
@@ -361,20 +352,17 @@ class TestTortoiseAdapterExceptions:
361
352
  """Test Tortoise adapter update_element_async with empty data."""
362
353
  try:
363
354
  from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
364
- from tortoise.models import Model
365
- from tortoise import fields
355
+ from tests.tortoise_models import SimpleTortoiseModel as TestModel
366
356
 
367
357
  adapter = TortoiseAdapter(
368
- database_url="sqlite:///test_tortoise_empty.db",
369
- models=[],
358
+ database_url="sqlite://:memory:",
359
+ models=["tests.tortoise_models"],
370
360
  app_label="test"
371
361
  )
372
362
 
373
- class TestModel(Model):
374
- id = fields.IntField(pk=True)
375
- name = fields.CharField(max_length=50)
376
-
363
+ from tortoise import Tortoise
377
364
  await adapter._ensure_initialized()
365
+ await Tortoise.generate_schemas(safe=True)
378
366
 
379
367
  # Create test data
380
368
  obj = await TestModel.create(name="test")
@@ -316,7 +316,8 @@ class TestMissingCoverage:
316
316
  assert result is True
317
317
 
318
318
  # Verify it's deleted
319
- assert TestModel.get_by_id(obj_id) is None
319
+ with pytest.raises(TestModel.DoesNotExist):
320
+ TestModel.get_by_id(obj_id)
320
321
 
321
322
  # Test with non-existent id
322
323
  with pytest.raises(HTTPException) as exc_info:
@@ -0,0 +1,100 @@
1
+ """Tests for TortoiseAdapter public lifecycle methods (initialize/close).
2
+
3
+ These tests use mocking so they run even when tortoise-orm is not installed
4
+ (which is the case in CI — only the [test] extra is installed there).
5
+ """
6
+
7
+ import pytest
8
+ from unittest.mock import AsyncMock, MagicMock, patch
9
+
10
+ from fastapi_viewsets.orm import tortoise_adapter as ta_module
11
+ from fastapi_viewsets.orm.tortoise_adapter import TortoiseAdapter
12
+
13
+
14
+ @pytest.fixture
15
+ def adapter():
16
+ """Create a TortoiseAdapter for testing, mocking TORTOISE_AVAILABLE."""
17
+ with patch.object(ta_module, "TORTOISE_AVAILABLE", True):
18
+ adapter = TortoiseAdapter(
19
+ database_url="sqlite://:memory:",
20
+ models=["tests.models"],
21
+ app_label="models",
22
+ )
23
+ return adapter
24
+
25
+
26
+ def _mock_tortoise():
27
+ """Create a MagicMock that simulates the Tortoise class."""
28
+ mock = MagicMock()
29
+ mock.generate_schemas = AsyncMock()
30
+ mock.close_connections = AsyncMock()
31
+ mock.init = AsyncMock()
32
+ return mock
33
+
34
+
35
+ @pytest.mark.unit
36
+ class TestTortoiseAdapterLifecycle:
37
+ """Tests for initialize() and close() public methods."""
38
+
39
+ @pytest.mark.async_test
40
+ async def test_initialize_without_schemas(self, adapter):
41
+ """initialize() calls _ensure_initialized but not generate_schemas."""
42
+ mock_tortoise = _mock_tortoise()
43
+ with patch.object(
44
+ adapter, "_ensure_initialized", new=AsyncMock()
45
+ ) as mock_init, patch.object(
46
+ ta_module, "Tortoise", mock_tortoise, create=True
47
+ ):
48
+ await adapter.initialize(generate_schemas=False)
49
+ mock_init.assert_awaited_once()
50
+ mock_tortoise.generate_schemas.assert_not_awaited()
51
+
52
+ @pytest.mark.async_test
53
+ async def test_initialize_with_schemas(self, adapter):
54
+ """initialize(generate_schemas=True) calls both init and generate_schemas."""
55
+ mock_tortoise = _mock_tortoise()
56
+ with patch.object(
57
+ adapter, "_ensure_initialized", new=AsyncMock()
58
+ ) as mock_init, patch.object(
59
+ ta_module, "Tortoise", mock_tortoise, create=True
60
+ ):
61
+ await adapter.initialize(generate_schemas=True)
62
+ mock_init.assert_awaited_once()
63
+ mock_tortoise.generate_schemas.assert_awaited_once_with(safe=True)
64
+
65
+ @pytest.mark.async_test
66
+ async def test_close_calls_tortoise_close(self, adapter):
67
+ """close() calls Tortoise.close_connections() and resets _initialized."""
68
+ mock_tortoise = _mock_tortoise()
69
+ adapter._initialized = True
70
+ with patch.object(
71
+ ta_module, "Tortoise", mock_tortoise, create=True
72
+ ):
73
+ await adapter.close()
74
+ mock_tortoise.close_connections.assert_awaited_once()
75
+ assert adapter._initialized is False
76
+
77
+ @pytest.mark.async_test
78
+ async def test_close_resets_initialized_even_if_already_false(self, adapter):
79
+ """close() works even when adapter was never initialized."""
80
+ mock_tortoise = _mock_tortoise()
81
+ assert adapter._initialized is False
82
+ with patch.object(
83
+ ta_module, "Tortoise", mock_tortoise, create=True
84
+ ):
85
+ await adapter.close()
86
+ mock_tortoise.close_connections.assert_awaited_once()
87
+ assert adapter._initialized is False
88
+
89
+ @pytest.mark.async_test
90
+ async def test_initialize_is_idempotent(self, adapter):
91
+ """Calling initialize() twice does not error."""
92
+ mock_tortoise = _mock_tortoise()
93
+ with patch.object(
94
+ adapter, "_ensure_initialized", new=AsyncMock()
95
+ ) as mock_init, patch.object(
96
+ ta_module, "Tortoise", mock_tortoise, create=True
97
+ ):
98
+ await adapter.initialize()
99
+ await adapter.initialize()
100
+ assert mock_init.await_count == 2