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.
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/PKG-INFO +54 -39
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/README.md +44 -35
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/tortoise_adapter.py +22 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/PKG-INFO +54 -39
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/SOURCES.txt +1 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/requires.txt +7 -2
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/pyproject.toml +8 -5
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_adapter_methods_coverage.py +20 -33
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_exception_handling.py +13 -25
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_missing_coverage.py +2 -1
- fastapi_viewsets-1.4.0/tests/test_tortoise_lifecycle.py +100 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/LICENSE +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/__init__.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/_compat.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/_register.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/async_base.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/async_utils.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/constants.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/db_conf.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/__init__.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/base.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/factory.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/peewee_adapter.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/sqlalchemy_adapter.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/serializer_utils.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/utils.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/dependency_links.txt +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/top_level.txt +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/setup.cfg +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/setup.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_base_viewset.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_driver_missing.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_async_utils.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_backward_compatibility.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_base_viewset.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_coverage_gaps.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_db_conf.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_db_conf_extended.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_edge_cases.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_error_handling.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_integration.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_orm_adapters.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_orm_adapters_extended.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_select_prefetch_related.py +0 -0
- {fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/tests/test_utils.py +0 -0
- {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
|
+
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/
|
|
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
|
|
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
|
|
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
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
55
61
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
56
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/
|
|
57
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
|
|
63
|
+
[](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
|
|
58
64
|
[](https://codecov.io/gh/svalench/fastapi_viewsets)
|
|
59
65
|
[](https://pepy.tech/project/fastapi-viewsets)
|
|
60
66
|
[](https://github.com/psf/black)
|
|
61
67
|
[](https://github.com/svalench/fastapi_viewsets/pulls)
|
|
68
|
+
[](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` (
|
|
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
|
-
@
|
|
205
|
-
async def
|
|
206
|
-
"""Open the Tortoise connection pool
|
|
214
|
+
@asynccontextmanager
|
|
215
|
+
async def lifespan(app: FastAPI):
|
|
216
|
+
"""Open the Tortoise connection pool at startup.
|
|
207
217
|
|
|
208
|
-
The adapter also
|
|
209
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
|
743
|
-
|
|
|
744
|
-
|
|
|
745
|
-
|
|
|
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
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
6
6
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
7
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/
|
|
8
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
|
|
8
|
+
[](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
|
|
9
9
|
[](https://codecov.io/gh/svalench/fastapi_viewsets)
|
|
10
10
|
[](https://pepy.tech/project/fastapi-viewsets)
|
|
11
11
|
[](https://github.com/psf/black)
|
|
12
12
|
[](https://github.com/svalench/fastapi_viewsets/pulls)
|
|
13
|
+
[](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` (
|
|
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
|
-
@
|
|
156
|
-
async def
|
|
157
|
-
"""Open the Tortoise connection pool
|
|
159
|
+
@asynccontextmanager
|
|
160
|
+
async def lifespan(app: FastAPI):
|
|
161
|
+
"""Open the Tortoise connection pool at startup.
|
|
158
162
|
|
|
159
|
-
The adapter also
|
|
160
|
-
|
|
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
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
|
694
|
-
|
|
|
695
|
-
|
|
|
696
|
-
|
|
|
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
|
+
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/
|
|
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
|
|
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
|
|
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
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
55
61
|
[](https://pypi.org/project/fastapi-viewsets/)
|
|
56
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/
|
|
57
|
-
[](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE)
|
|
63
|
+
[](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
|
|
58
64
|
[](https://codecov.io/gh/svalench/fastapi_viewsets)
|
|
59
65
|
[](https://pepy.tech/project/fastapi-viewsets)
|
|
60
66
|
[](https://github.com/psf/black)
|
|
61
67
|
[](https://github.com/svalench/fastapi_viewsets/pulls)
|
|
68
|
+
[](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` (
|
|
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
|
-
@
|
|
205
|
-
async def
|
|
206
|
-
"""Open the Tortoise connection pool
|
|
214
|
+
@asynccontextmanager
|
|
215
|
+
async def lifespan(app: FastAPI):
|
|
216
|
+
"""Open the Tortoise connection pool at startup.
|
|
207
217
|
|
|
208
|
-
The adapter also
|
|
209
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
|
743
|
-
|
|
|
744
|
-
|
|
|
745
|
-
|
|
|
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,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
|
|
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
|
|
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.
|
|
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/
|
|
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
|
|
190
|
-
|
|
191
|
-
|
|
189
|
+
from tests.tortoise_models import SimpleTortoiseModel as TestModel
|
|
190
|
+
|
|
192
191
|
adapter = TortoiseAdapter(
|
|
193
|
-
database_url="sqlite
|
|
194
|
-
models=[],
|
|
192
|
+
database_url="sqlite://:memory:",
|
|
193
|
+
models=["tests.tortoise_models"],
|
|
195
194
|
app_label="test"
|
|
196
195
|
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
232
|
-
|
|
233
|
-
|
|
228
|
+
from tests.tortoise_models import SimpleTortoiseModel as TestModel
|
|
229
|
+
|
|
234
230
|
adapter = TortoiseAdapter(
|
|
235
|
-
database_url="sqlite
|
|
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
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
from tests.tortoise_models import SimpleTortoiseModel as TestModel
|
|
261
|
+
|
|
272
262
|
adapter = TortoiseAdapter(
|
|
273
|
-
database_url="sqlite
|
|
274
|
-
models=[],
|
|
263
|
+
database_url="sqlite://:memory:",
|
|
264
|
+
models=["tests.tortoise_models"],
|
|
275
265
|
app_label="test"
|
|
276
266
|
)
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
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
|
|
289
|
-
models=[],
|
|
286
|
+
database_url="sqlite://:memory:",
|
|
287
|
+
models=["tests.tortoise_models"],
|
|
290
288
|
app_label="test"
|
|
291
289
|
)
|
|
292
290
|
|
|
293
|
-
|
|
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
|
|
326
|
-
from tortoise import fields
|
|
321
|
+
from tests.tortoise_models import SimpleTortoiseModel as TestModel
|
|
327
322
|
|
|
328
323
|
adapter = TortoiseAdapter(
|
|
329
|
-
database_url="sqlite
|
|
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
|
|
365
|
-
from tortoise import fields
|
|
355
|
+
from tests.tortoise_models import SimpleTortoiseModel as TestModel
|
|
366
356
|
|
|
367
357
|
adapter = TortoiseAdapter(
|
|
368
|
-
database_url="sqlite
|
|
369
|
-
models=[],
|
|
358
|
+
database_url="sqlite://:memory:",
|
|
359
|
+
models=["tests.tortoise_models"],
|
|
370
360
|
app_label="test"
|
|
371
361
|
)
|
|
372
362
|
|
|
373
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets/orm/sqlalchemy_adapter.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_viewsets-1.3.0 → fastapi_viewsets-1.4.0}/fastapi_viewsets.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|