fastapi-viewsets 1.1.0__tar.gz → 1.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fastapi_viewsets-1.2.1/PKG-INFO +291 -0
- fastapi_viewsets-1.2.1/README.md +242 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/__init__.py +181 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/_compat.py +71 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/_register.py +133 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/async_base.py +165 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/db_conf.py +171 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/__init__.py +26 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/base.py +287 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/factory.py +181 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/peewee_adapter.py +283 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/sqlalchemy_adapter.py +498 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets/orm/tortoise_adapter.py +279 -0
- fastapi_viewsets-1.2.1/fastapi_viewsets.egg-info/PKG-INFO +291 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets.egg-info/SOURCES.txt +10 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets.egg-info/requires.txt +6 -3
- fastapi_viewsets-1.2.1/pyproject.toml +83 -0
- fastapi_viewsets-1.2.1/setup.py +10 -0
- fastapi_viewsets-1.2.1/tests/test_async_driver_missing.py +57 -0
- fastapi_viewsets-1.1.0/PKG-INFO +0 -454
- fastapi_viewsets-1.1.0/README.md +0 -412
- fastapi_viewsets-1.1.0/fastapi_viewsets/__init__.py +0 -276
- fastapi_viewsets-1.1.0/fastapi_viewsets/async_base.py +0 -276
- fastapi_viewsets-1.1.0/fastapi_viewsets/db_conf.py +0 -134
- fastapi_viewsets-1.1.0/fastapi_viewsets.egg-info/PKG-INFO +0 -454
- fastapi_viewsets-1.1.0/setup.py +0 -50
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/LICENSE +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets/async_utils.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets/constants.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets/utils.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets.egg-info/dependency_links.txt +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/fastapi_viewsets.egg-info/top_level.txt +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/setup.cfg +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_adapter_methods_coverage.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_async_base_viewset.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_async_utils.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_backward_compatibility.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_base_viewset.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_coverage_gaps.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_db_conf.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_db_conf_extended.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_edge_cases.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_error_handling.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_exception_handling.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_integration.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_missing_coverage.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_orm_adapters.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_orm_adapters_extended.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_utils.py +0 -0
- {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.1}/tests/test_viewsets_with_adapters.py +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastapi_viewsets
|
|
3
|
+
Version: 1.2.1
|
|
4
|
+
Summary: DRF-style viewsets for FastAPI with SQLAlchemy/Tortoise/Peewee adapters and Pydantic v2 support.
|
|
5
|
+
Author: Alexander Valenchits
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/svalench/fastapi_viewsets
|
|
8
|
+
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
|
+
Keywords: fastapi,viewsets,crud,sqlalchemy,tortoise,peewee,pydantic
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Framework :: FastAPI
|
|
20
|
+
Classifier: Framework :: Pydantic :: 2
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: fastapi>=0.110.0
|
|
26
|
+
Requires-Dist: uvicorn>=0.17.6
|
|
27
|
+
Requires-Dist: SQLAlchemy>=1.4.36
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.5
|
|
29
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
30
|
+
Provides-Extra: sqlalchemy
|
|
31
|
+
Requires-Dist: SQLAlchemy>=1.4.36; extra == "sqlalchemy"
|
|
32
|
+
Provides-Extra: tortoise
|
|
33
|
+
Requires-Dist: tortoise-orm>=0.20.0; extra == "tortoise"
|
|
34
|
+
Requires-Dist: asyncpg>=0.28.0; extra == "tortoise"
|
|
35
|
+
Provides-Extra: peewee
|
|
36
|
+
Requires-Dist: peewee>=3.17.0; extra == "peewee"
|
|
37
|
+
Provides-Extra: test
|
|
38
|
+
Requires-Dist: pytest>=7.0.0; extra == "test"
|
|
39
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "test"
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "test"
|
|
41
|
+
Requires-Dist: httpx>=0.24.0; extra == "test"
|
|
42
|
+
Requires-Dist: faker>=18.0.0; extra == "test"
|
|
43
|
+
Requires-Dist: aiosqlite>=0.19.0; extra == "test"
|
|
44
|
+
Provides-Extra: lint
|
|
45
|
+
Requires-Dist: ruff>=0.5; extra == "lint"
|
|
46
|
+
Requires-Dist: black>=24; extra == "lint"
|
|
47
|
+
Requires-Dist: mypy>=1.8; extra == "lint"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# fastapi-viewsets
|
|
51
|
+
|
|
52
|
+
Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoints from SQLAlchemy, Tortoise ORM, or Peewee models in minutes.
|
|
53
|
+
|
|
54
|
+
[](https://pypi.org/project/fastapi-viewsets/)
|
|
55
|
+
[](https://pypi.org/project/fastapi-viewsets/)
|
|
56
|
+
[](https://github.com/svalench/fastapi_viewsets/blob/main/LICENSE)
|
|
57
|
+
[](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
|
|
58
|
+
[](https://codecov.io/gh/svalench/fastapi_viewsets)
|
|
59
|
+
[](https://pepy.tech/project/fastapi-viewsets)
|
|
60
|
+
[](https://github.com/psf/black)
|
|
61
|
+
[](https://github.com/svalench/fastapi_viewsets/pulls)
|
|
62
|
+
|
|
63
|
+
## Why fastapi-viewsets
|
|
64
|
+
|
|
65
|
+
- **DRF-style ergonomics** on top of FastAPI routers and dependency injection.
|
|
66
|
+
- **Less boilerplate** — register LIST, GET, POST, PUT, PATCH, and DELETE from one class.
|
|
67
|
+
- **ORM-agnostic core** — pluggable adapters for SQLAlchemy (sync/async), Tortoise ORM, and Peewee (`ORM_TYPE` / optional extras).
|
|
68
|
+
- **Typed, Pydantic-first responses** with OpenAPI tags and schemas generated from your `response_model`.
|
|
69
|
+
- **Built-in list pagination** (`limit` / `offset`), optional OAuth2 on selected operations, and room to grow for search and richer filters (see Roadmap).
|
|
70
|
+
|
|
71
|
+
## Feature matrix
|
|
72
|
+
|
|
73
|
+
| Feature | SQLAlchemy (sync) | SQLAlchemy (async) | Tortoise ORM | Peewee |
|
|
74
|
+
| --- | --- | --- | --- | --- |
|
|
75
|
+
| `BaseViewset` / `AsyncBaseViewset` CRUD | Supported | Supported (`AsyncBaseViewset`) | Supported via adapter + async session | Supported via adapter |
|
|
76
|
+
| `limit` / `offset` on LIST | Supported | Supported | Supported | Supported |
|
|
77
|
+
| OAuth2 on selected methods (`register`) | Supported | Supported | Supported | Supported |
|
|
78
|
+
| `search` query on LIST (server-side) | **Roadmap** | **Roadmap** | **Roadmap** | **Roadmap** |
|
|
79
|
+
| Declarative ordering / advanced filters | **Roadmap** | **Roadmap** | **Roadmap** | **Roadmap** |
|
|
80
|
+
|
|
81
|
+
## Installation
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pip install fastapi-viewsets
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Optional extras (see `setup.py`):
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
pip install "fastapi-viewsets[sqlalchemy]"
|
|
91
|
+
pip install "fastapi-viewsets[tortoise]"
|
|
92
|
+
pip install "fastapi-viewsets[peewee]"
|
|
93
|
+
pip install "fastapi-viewsets[test]" # pytest, httpx, coverage, etc.
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For async SQLAlchemy you still need a driver such as `aiosqlite`, `asyncpg`, or `aiomysql` alongside your database URL.
|
|
97
|
+
|
|
98
|
+
## Quickstart (SQLAlchemy, sync)
|
|
99
|
+
|
|
100
|
+
Save as `main.py` in an empty folder and run `python main.py` or `uvicorn main:app --reload`.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from fastapi import FastAPI
|
|
104
|
+
from pydantic import BaseModel, ConfigDict
|
|
105
|
+
from sqlalchemy import Column, Integer, String
|
|
106
|
+
|
|
107
|
+
from fastapi_viewsets import BaseViewset
|
|
108
|
+
from fastapi_viewsets.db_conf import Base, engine, get_session
|
|
109
|
+
|
|
110
|
+
app = FastAPI()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Item(Base):
|
|
114
|
+
"""Example SQLAlchemy model."""
|
|
115
|
+
|
|
116
|
+
__tablename__ = "items"
|
|
117
|
+
id = Column(Integer, primary_key=True)
|
|
118
|
+
name = Column(String(255), nullable=False)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ItemSchema(BaseModel):
|
|
122
|
+
"""Pydantic model for request and response bodies."""
|
|
123
|
+
|
|
124
|
+
model_config = ConfigDict(from_attributes=True)
|
|
125
|
+
id: int | None = None
|
|
126
|
+
name: str
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
Base.metadata.create_all(bind=engine)
|
|
130
|
+
items = BaseViewset(endpoint="/items", model=Item, response_model=ItemSchema, db_session=get_session, tags=["items"])
|
|
131
|
+
items.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"])
|
|
132
|
+
app.include_router(items)
|
|
133
|
+
|
|
134
|
+
if __name__ == "__main__":
|
|
135
|
+
import uvicorn
|
|
136
|
+
|
|
137
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
`GET /items` returns `200` with a JSON list (possibly empty). Use `POST /items` with `{"name": "apple"}` to create rows.
|
|
141
|
+
|
|
142
|
+
## Authentication example
|
|
143
|
+
|
|
144
|
+
`register()` accepts `OAuth2PasswordBearer` plus a list of logical operations (`POST`, `PUT`, …) that require a bearer token.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from fastapi import FastAPI
|
|
148
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
149
|
+
from pydantic import BaseModel, ConfigDict
|
|
150
|
+
from sqlalchemy import Column, Integer, String
|
|
151
|
+
from fastapi_viewsets import BaseViewset
|
|
152
|
+
from fastapi_viewsets.db_conf import Base, engine, get_session
|
|
153
|
+
|
|
154
|
+
app = FastAPI()
|
|
155
|
+
oauth2 = OAuth2PasswordBearer(tokenUrl="/token")
|
|
156
|
+
|
|
157
|
+
class Item(Base):
|
|
158
|
+
"""SQLAlchemy model for OAuth2-protected writes."""
|
|
159
|
+
|
|
160
|
+
__tablename__ = "items_oauth"
|
|
161
|
+
id = Column(Integer, primary_key=True)
|
|
162
|
+
name = Column(String(255), nullable=False)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ItemSchema(BaseModel):
|
|
166
|
+
"""Pydantic schema for Item payloads and responses."""
|
|
167
|
+
|
|
168
|
+
model_config = ConfigDict(from_attributes=True)
|
|
169
|
+
id: int | None = None
|
|
170
|
+
name: str
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
Base.metadata.create_all(bind=engine)
|
|
174
|
+
router = BaseViewset(endpoint="/items", model=Item, response_model=ItemSchema, db_session=get_session, tags=["items"])
|
|
175
|
+
router.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"], oauth_protect=oauth2, protected_methods=["POST", "PATCH", "DELETE"])
|
|
176
|
+
app.include_router(router)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Pagination, filtering, ordering
|
|
180
|
+
|
|
181
|
+
**Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route.
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
from fastapi_viewsets import BaseViewset
|
|
185
|
+
|
|
186
|
+
def pagination_hint() -> str:
|
|
187
|
+
"""Document LIST pagination after `register()` (e.g. GET /items?limit=10&offset=20)."""
|
|
188
|
+
return "limit and offset are parsed by `BaseViewset.list`"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Filtering** — `list` accepts `search`, but ORM adapters ignore it today; server-side search is on the Roadmap. Subclass `BaseViewset` and override `list()` with your own query until then.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from fastapi_viewsets import BaseViewset
|
|
195
|
+
|
|
196
|
+
def filtering_hint() -> str:
|
|
197
|
+
"""Explain that `search` is reserved; override `list` for real filters today."""
|
|
198
|
+
return "search parameter is not yet applied in adapters"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Ordering** — there is no shared `order_by` helper yet; override `list()` with an ordered query or wait for the Roadmap.
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from fastapi_viewsets import BaseViewset
|
|
205
|
+
|
|
206
|
+
def ordering_hint() -> str:
|
|
207
|
+
"""Note the absence of a built-in ordering helper on LIST endpoints."""
|
|
208
|
+
return "override list or wait for roadmap ordering helpers"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Permissions and custom routes
|
|
212
|
+
|
|
213
|
+
There is no `get_queryset` hook; scope queries by subclassing `BaseViewset` and overriding `list()`, `get_element()`, or related handlers. The class subclasses `APIRouter`, so attach extra endpoints with `add_api_route` **before** `register()` if paths must win over `/{id}`:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
from fastapi_viewsets import BaseViewset
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class ItemsWithStats(BaseViewset):
|
|
220
|
+
"""Adds a custom read-only route alongside generated CRUD."""
|
|
221
|
+
|
|
222
|
+
def __init__(self, *args, **kwargs):
|
|
223
|
+
"""Register static paths before CRUD routes."""
|
|
224
|
+
super().__init__(*args, **kwargs)
|
|
225
|
+
self.add_api_route(
|
|
226
|
+
f"{self.endpoint}/stats",
|
|
227
|
+
self.collection_stats,
|
|
228
|
+
methods=["GET"],
|
|
229
|
+
tags=self.tags or [],
|
|
230
|
+
name="items_stats",
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def collection_stats(self) -> dict[str, str]:
|
|
234
|
+
"""Return a minimal summary for monitoring or health checks."""
|
|
235
|
+
return {"resource": self.endpoint.strip("/")}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# Instantiate with model, response_model, and db_session (see quickstart), then call register().
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
## What is new (v1.2.0)
|
|
242
|
+
|
|
243
|
+
- Pydantic v2 first: CRUD handlers use `model_dump(exclude_unset=...)`, fixing PATCH semantics that previously overwrote unset fields with defaults.
|
|
244
|
+
- Lazy `db_conf`: importing the package no longer creates SQLAlchemy engines unless they are needed, and works without async drivers installed.
|
|
245
|
+
- Single source of truth for sync→async URL conversion and the default adapter singleton.
|
|
246
|
+
- Internal `register()` deduplicated between sync and async viewsets via a shared mixin.
|
|
247
|
+
- PEP 621 `pyproject.toml`, `python_requires>=3.9`, FastAPI `>=0.110`, ruff/black/mypy preconfigured.
|
|
248
|
+
|
|
249
|
+
Previous release: [v1.1.0](RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.
|
|
250
|
+
|
|
251
|
+
Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.md), [RELEASE_1.1.0.md](RELEASE_1.1.0.md).
|
|
252
|
+
|
|
253
|
+
## Roadmap (planned)
|
|
254
|
+
|
|
255
|
+
| Item | Target | Status |
|
|
256
|
+
| --- | --- | --- |
|
|
257
|
+
| Dedicated `AsyncModelViewSet` ergonomics on top of SQLAlchemy 2.x async sessions | v1.2 | Planned |
|
|
258
|
+
| First-class Tortoise ORM viewset examples and docs (`TortoiseModelViewSet` naming TBD) | v1.2 | Planned |
|
|
259
|
+
| Async pagination helpers and transaction boundaries across adapters | v1.3 | Planned |
|
|
260
|
+
| Richer OpenAPI for nested Pydantic models | v1.3 | Planned |
|
|
261
|
+
| Wire `search` on LIST to real database queries | v1.2 | Planned |
|
|
262
|
+
|
|
263
|
+
## Comparison with alternatives
|
|
264
|
+
|
|
265
|
+
| Approach | Developer experience | ORM support | Permissions | Filtering |
|
|
266
|
+
| --- | --- | --- | --- | --- |
|
|
267
|
+
| fastapi-viewsets | One `BaseViewset` registers CRUD routes | SQLAlchemy sync/async, Tortoise, Peewee via adapters | OAuth2 per logical method via `register` | `limit`/`offset` today; `search` and advanced filters on Roadmap |
|
|
268
|
+
| fastapi-crudrouter | CRUD-focused generators, less ViewSet-shaped | Primarily SQLAlchemy | Custom middleware/deps | Often extended manually |
|
|
269
|
+
| Hand-rolled FastAPI | Full control, most boilerplate | Any ORM you integrate | Fully custom | Fully custom |
|
|
270
|
+
|
|
271
|
+
## Testing
|
|
272
|
+
|
|
273
|
+
From the repository root (see `pytest.ini`):
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
pytest
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
Coverage is enforced with `--cov-fail-under=70` (HTML and XML reports are emitted for local inspection).
|
|
280
|
+
|
|
281
|
+
## Contributing
|
|
282
|
+
|
|
283
|
+
See [open issues](https://github.com/svalench/fastapi_viewsets/issues) to propose changes; pull requests are welcome.
|
|
284
|
+
|
|
285
|
+
## License
|
|
286
|
+
|
|
287
|
+
Distributed under the MIT License. See [LICENSE](LICENSE).
|
|
288
|
+
|
|
289
|
+
## Author
|
|
290
|
+
|
|
291
|
+
Built by [Alexander Valenchits](https://github.com/svalench) — Tech Lead @ AluSoft, Minsk.
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# fastapi-viewsets
|
|
2
|
+
|
|
3
|
+
Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoints from SQLAlchemy, Tortoise ORM, or Peewee models in minutes.
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/fastapi-viewsets/)
|
|
6
|
+
[](https://pypi.org/project/fastapi-viewsets/)
|
|
7
|
+
[](https://github.com/svalench/fastapi_viewsets/blob/main/LICENSE)
|
|
8
|
+
[](https://github.com/svalench/fastapi_viewsets/actions/workflows/test.yml)
|
|
9
|
+
[](https://codecov.io/gh/svalench/fastapi_viewsets)
|
|
10
|
+
[](https://pepy.tech/project/fastapi-viewsets)
|
|
11
|
+
[](https://github.com/psf/black)
|
|
12
|
+
[](https://github.com/svalench/fastapi_viewsets/pulls)
|
|
13
|
+
|
|
14
|
+
## Why fastapi-viewsets
|
|
15
|
+
|
|
16
|
+
- **DRF-style ergonomics** on top of FastAPI routers and dependency injection.
|
|
17
|
+
- **Less boilerplate** — register LIST, GET, POST, PUT, PATCH, and DELETE from one class.
|
|
18
|
+
- **ORM-agnostic core** — pluggable adapters for SQLAlchemy (sync/async), Tortoise ORM, and Peewee (`ORM_TYPE` / optional extras).
|
|
19
|
+
- **Typed, Pydantic-first responses** with OpenAPI tags and schemas generated from your `response_model`.
|
|
20
|
+
- **Built-in list pagination** (`limit` / `offset`), optional OAuth2 on selected operations, and room to grow for search and richer filters (see Roadmap).
|
|
21
|
+
|
|
22
|
+
## Feature matrix
|
|
23
|
+
|
|
24
|
+
| Feature | SQLAlchemy (sync) | SQLAlchemy (async) | Tortoise ORM | Peewee |
|
|
25
|
+
| --- | --- | --- | --- | --- |
|
|
26
|
+
| `BaseViewset` / `AsyncBaseViewset` CRUD | Supported | Supported (`AsyncBaseViewset`) | Supported via adapter + async session | Supported via adapter |
|
|
27
|
+
| `limit` / `offset` on LIST | Supported | Supported | Supported | Supported |
|
|
28
|
+
| OAuth2 on selected methods (`register`) | Supported | Supported | Supported | Supported |
|
|
29
|
+
| `search` query on LIST (server-side) | **Roadmap** | **Roadmap** | **Roadmap** | **Roadmap** |
|
|
30
|
+
| Declarative ordering / advanced filters | **Roadmap** | **Roadmap** | **Roadmap** | **Roadmap** |
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install fastapi-viewsets
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Optional extras (see `setup.py`):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "fastapi-viewsets[sqlalchemy]"
|
|
42
|
+
pip install "fastapi-viewsets[tortoise]"
|
|
43
|
+
pip install "fastapi-viewsets[peewee]"
|
|
44
|
+
pip install "fastapi-viewsets[test]" # pytest, httpx, coverage, etc.
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For async SQLAlchemy you still need a driver such as `aiosqlite`, `asyncpg`, or `aiomysql` alongside your database URL.
|
|
48
|
+
|
|
49
|
+
## Quickstart (SQLAlchemy, sync)
|
|
50
|
+
|
|
51
|
+
Save as `main.py` in an empty folder and run `python main.py` or `uvicorn main:app --reload`.
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from fastapi import FastAPI
|
|
55
|
+
from pydantic import BaseModel, ConfigDict
|
|
56
|
+
from sqlalchemy import Column, Integer, String
|
|
57
|
+
|
|
58
|
+
from fastapi_viewsets import BaseViewset
|
|
59
|
+
from fastapi_viewsets.db_conf import Base, engine, get_session
|
|
60
|
+
|
|
61
|
+
app = FastAPI()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Item(Base):
|
|
65
|
+
"""Example SQLAlchemy model."""
|
|
66
|
+
|
|
67
|
+
__tablename__ = "items"
|
|
68
|
+
id = Column(Integer, primary_key=True)
|
|
69
|
+
name = Column(String(255), nullable=False)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class ItemSchema(BaseModel):
|
|
73
|
+
"""Pydantic model for request and response bodies."""
|
|
74
|
+
|
|
75
|
+
model_config = ConfigDict(from_attributes=True)
|
|
76
|
+
id: int | None = None
|
|
77
|
+
name: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
Base.metadata.create_all(bind=engine)
|
|
81
|
+
items = BaseViewset(endpoint="/items", model=Item, response_model=ItemSchema, db_session=get_session, tags=["items"])
|
|
82
|
+
items.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"])
|
|
83
|
+
app.include_router(items)
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
import uvicorn
|
|
87
|
+
|
|
88
|
+
uvicorn.run(app, host="127.0.0.1", port=8000)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`GET /items` returns `200` with a JSON list (possibly empty). Use `POST /items` with `{"name": "apple"}` to create rows.
|
|
92
|
+
|
|
93
|
+
## Authentication example
|
|
94
|
+
|
|
95
|
+
`register()` accepts `OAuth2PasswordBearer` plus a list of logical operations (`POST`, `PUT`, …) that require a bearer token.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from fastapi import FastAPI
|
|
99
|
+
from fastapi.security import OAuth2PasswordBearer
|
|
100
|
+
from pydantic import BaseModel, ConfigDict
|
|
101
|
+
from sqlalchemy import Column, Integer, String
|
|
102
|
+
from fastapi_viewsets import BaseViewset
|
|
103
|
+
from fastapi_viewsets.db_conf import Base, engine, get_session
|
|
104
|
+
|
|
105
|
+
app = FastAPI()
|
|
106
|
+
oauth2 = OAuth2PasswordBearer(tokenUrl="/token")
|
|
107
|
+
|
|
108
|
+
class Item(Base):
|
|
109
|
+
"""SQLAlchemy model for OAuth2-protected writes."""
|
|
110
|
+
|
|
111
|
+
__tablename__ = "items_oauth"
|
|
112
|
+
id = Column(Integer, primary_key=True)
|
|
113
|
+
name = Column(String(255), nullable=False)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ItemSchema(BaseModel):
|
|
117
|
+
"""Pydantic schema for Item payloads and responses."""
|
|
118
|
+
|
|
119
|
+
model_config = ConfigDict(from_attributes=True)
|
|
120
|
+
id: int | None = None
|
|
121
|
+
name: str
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
Base.metadata.create_all(bind=engine)
|
|
125
|
+
router = BaseViewset(endpoint="/items", model=Item, response_model=ItemSchema, db_session=get_session, tags=["items"])
|
|
126
|
+
router.register(methods=["LIST", "GET", "POST", "PATCH", "DELETE"], oauth_protect=oauth2, protected_methods=["POST", "PATCH", "DELETE"])
|
|
127
|
+
app.include_router(router)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Pagination, filtering, ordering
|
|
131
|
+
|
|
132
|
+
**Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from fastapi_viewsets import BaseViewset
|
|
136
|
+
|
|
137
|
+
def pagination_hint() -> str:
|
|
138
|
+
"""Document LIST pagination after `register()` (e.g. GET /items?limit=10&offset=20)."""
|
|
139
|
+
return "limit and offset are parsed by `BaseViewset.list`"
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**Filtering** — `list` accepts `search`, but ORM adapters ignore it today; server-side search is on the Roadmap. Subclass `BaseViewset` and override `list()` with your own query until then.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from fastapi_viewsets import BaseViewset
|
|
146
|
+
|
|
147
|
+
def filtering_hint() -> str:
|
|
148
|
+
"""Explain that `search` is reserved; override `list` for real filters today."""
|
|
149
|
+
return "search parameter is not yet applied in adapters"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Ordering** — there is no shared `order_by` helper yet; override `list()` with an ordered query or wait for the Roadmap.
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from fastapi_viewsets import BaseViewset
|
|
156
|
+
|
|
157
|
+
def ordering_hint() -> str:
|
|
158
|
+
"""Note the absence of a built-in ordering helper on LIST endpoints."""
|
|
159
|
+
return "override list or wait for roadmap ordering helpers"
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Permissions and custom routes
|
|
163
|
+
|
|
164
|
+
There is no `get_queryset` hook; scope queries by subclassing `BaseViewset` and overriding `list()`, `get_element()`, or related handlers. The class subclasses `APIRouter`, so attach extra endpoints with `add_api_route` **before** `register()` if paths must win over `/{id}`:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from fastapi_viewsets import BaseViewset
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ItemsWithStats(BaseViewset):
|
|
171
|
+
"""Adds a custom read-only route alongside generated CRUD."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, *args, **kwargs):
|
|
174
|
+
"""Register static paths before CRUD routes."""
|
|
175
|
+
super().__init__(*args, **kwargs)
|
|
176
|
+
self.add_api_route(
|
|
177
|
+
f"{self.endpoint}/stats",
|
|
178
|
+
self.collection_stats,
|
|
179
|
+
methods=["GET"],
|
|
180
|
+
tags=self.tags or [],
|
|
181
|
+
name="items_stats",
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def collection_stats(self) -> dict[str, str]:
|
|
185
|
+
"""Return a minimal summary for monitoring or health checks."""
|
|
186
|
+
return {"resource": self.endpoint.strip("/")}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Instantiate with model, response_model, and db_session (see quickstart), then call register().
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## What is new (v1.2.0)
|
|
193
|
+
|
|
194
|
+
- Pydantic v2 first: CRUD handlers use `model_dump(exclude_unset=...)`, fixing PATCH semantics that previously overwrote unset fields with defaults.
|
|
195
|
+
- Lazy `db_conf`: importing the package no longer creates SQLAlchemy engines unless they are needed, and works without async drivers installed.
|
|
196
|
+
- Single source of truth for sync→async URL conversion and the default adapter singleton.
|
|
197
|
+
- Internal `register()` deduplicated between sync and async viewsets via a shared mixin.
|
|
198
|
+
- PEP 621 `pyproject.toml`, `python_requires>=3.9`, FastAPI `>=0.110`, ruff/black/mypy preconfigured.
|
|
199
|
+
|
|
200
|
+
Previous release: [v1.1.0](RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.
|
|
201
|
+
|
|
202
|
+
Details: [RELEASE_NOTES.md](RELEASE_NOTES.md), [RELEASE_1.2.0.md](RELEASE_1.2.0.md), [RELEASE_1.1.0.md](RELEASE_1.1.0.md).
|
|
203
|
+
|
|
204
|
+
## Roadmap (planned)
|
|
205
|
+
|
|
206
|
+
| Item | Target | Status |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| Dedicated `AsyncModelViewSet` ergonomics on top of SQLAlchemy 2.x async sessions | v1.2 | Planned |
|
|
209
|
+
| First-class Tortoise ORM viewset examples and docs (`TortoiseModelViewSet` naming TBD) | v1.2 | Planned |
|
|
210
|
+
| Async pagination helpers and transaction boundaries across adapters | v1.3 | Planned |
|
|
211
|
+
| Richer OpenAPI for nested Pydantic models | v1.3 | Planned |
|
|
212
|
+
| Wire `search` on LIST to real database queries | v1.2 | Planned |
|
|
213
|
+
|
|
214
|
+
## Comparison with alternatives
|
|
215
|
+
|
|
216
|
+
| Approach | Developer experience | ORM support | Permissions | Filtering |
|
|
217
|
+
| --- | --- | --- | --- | --- |
|
|
218
|
+
| fastapi-viewsets | One `BaseViewset` registers CRUD routes | SQLAlchemy sync/async, Tortoise, Peewee via adapters | OAuth2 per logical method via `register` | `limit`/`offset` today; `search` and advanced filters on Roadmap |
|
|
219
|
+
| fastapi-crudrouter | CRUD-focused generators, less ViewSet-shaped | Primarily SQLAlchemy | Custom middleware/deps | Often extended manually |
|
|
220
|
+
| Hand-rolled FastAPI | Full control, most boilerplate | Any ORM you integrate | Fully custom | Fully custom |
|
|
221
|
+
|
|
222
|
+
## Testing
|
|
223
|
+
|
|
224
|
+
From the repository root (see `pytest.ini`):
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Coverage is enforced with `--cov-fail-under=70` (HTML and XML reports are emitted for local inspection).
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
See [open issues](https://github.com/svalench/fastapi_viewsets/issues) to propose changes; pull requests are welcome.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
Distributed under the MIT License. See [LICENSE](LICENSE).
|
|
239
|
+
|
|
240
|
+
## Author
|
|
241
|
+
|
|
242
|
+
Built by [Alexander Valenchits](https://github.com/svalench) — Tech Lead @ AluSoft, Minsk.
|