fastapi-viewsets 1.1.0__tar.gz → 1.2.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 (49) hide show
  1. fastapi_viewsets-1.2.0/PKG-INFO +291 -0
  2. fastapi_viewsets-1.2.0/README.md +242 -0
  3. fastapi_viewsets-1.2.0/fastapi_viewsets/__init__.py +181 -0
  4. fastapi_viewsets-1.2.0/fastapi_viewsets/_compat.py +71 -0
  5. fastapi_viewsets-1.2.0/fastapi_viewsets/_register.py +133 -0
  6. fastapi_viewsets-1.2.0/fastapi_viewsets/async_base.py +165 -0
  7. fastapi_viewsets-1.2.0/fastapi_viewsets/db_conf.py +171 -0
  8. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/__init__.py +26 -0
  9. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/base.py +287 -0
  10. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/factory.py +181 -0
  11. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/peewee_adapter.py +283 -0
  12. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/sqlalchemy_adapter.py +492 -0
  13. fastapi_viewsets-1.2.0/fastapi_viewsets/orm/tortoise_adapter.py +279 -0
  14. fastapi_viewsets-1.2.0/fastapi_viewsets.egg-info/PKG-INFO +291 -0
  15. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets.egg-info/SOURCES.txt +9 -0
  16. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets.egg-info/requires.txt +6 -3
  17. fastapi_viewsets-1.2.0/pyproject.toml +81 -0
  18. fastapi_viewsets-1.2.0/setup.py +10 -0
  19. fastapi_viewsets-1.1.0/PKG-INFO +0 -454
  20. fastapi_viewsets-1.1.0/README.md +0 -412
  21. fastapi_viewsets-1.1.0/fastapi_viewsets/__init__.py +0 -276
  22. fastapi_viewsets-1.1.0/fastapi_viewsets/async_base.py +0 -276
  23. fastapi_viewsets-1.1.0/fastapi_viewsets/db_conf.py +0 -134
  24. fastapi_viewsets-1.1.0/fastapi_viewsets.egg-info/PKG-INFO +0 -454
  25. fastapi_viewsets-1.1.0/setup.py +0 -50
  26. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/LICENSE +0 -0
  27. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets/async_utils.py +0 -0
  28. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets/constants.py +0 -0
  29. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets/utils.py +0 -0
  30. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets.egg-info/dependency_links.txt +0 -0
  31. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/fastapi_viewsets.egg-info/top_level.txt +0 -0
  32. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/setup.cfg +0 -0
  33. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_adapter_methods_coverage.py +0 -0
  34. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_async_base_viewset.py +0 -0
  35. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_async_utils.py +0 -0
  36. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_backward_compatibility.py +0 -0
  37. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_base_viewset.py +0 -0
  38. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_coverage_gaps.py +0 -0
  39. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_db_conf.py +0 -0
  40. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_db_conf_extended.py +0 -0
  41. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_edge_cases.py +0 -0
  42. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_error_handling.py +0 -0
  43. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_exception_handling.py +0 -0
  44. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_integration.py +0 -0
  45. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_missing_coverage.py +0 -0
  46. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_orm_adapters.py +0 -0
  47. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_orm_adapters_extended.py +0 -0
  48. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/tests/test_utils.py +0 -0
  49. {fastapi_viewsets-1.1.0 → fastapi_viewsets-1.2.0}/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.0
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
+ [![PyPI version](https://badge.fury.io/py/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
55
+ [![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)
58
+ [![codecov](https://codecov.io/gh/svalench/fastapi_viewsets/graph/badge.svg)](https://codecov.io/gh/svalench/fastapi_viewsets)
59
+ [![Downloads/month](https://static.pepy.tech/badge/fastapi-viewsets/month)](https://pepy.tech/project/fastapi-viewsets)
60
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
61
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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
+ [![PyPI version](https://badge.fury.io/py/fastapi-viewsets.svg)](https://pypi.org/project/fastapi-viewsets/)
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)
9
+ [![codecov](https://codecov.io/gh/svalench/fastapi_viewsets/graph/badge.svg)](https://codecov.io/gh/svalench/fastapi_viewsets)
10
+ [![Downloads/month](https://static.pepy.tech/badge/fastapi-viewsets/month)](https://pepy.tech/project/fastapi-viewsets)
11
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
12
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](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.