fastapi-viewsets 1.5.0__tar.gz → 1.5.2__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 (71) hide show
  1. fastapi_viewsets-1.5.2/MANIFEST.in +3 -0
  2. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/PKG-INFO +68 -14
  3. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/README.md +64 -10
  4. fastapi_viewsets-1.5.2/docs/api-reference.md +311 -0
  5. fastapi_viewsets-1.5.2/docs/assets/custom.css +25 -0
  6. fastapi_viewsets-1.5.2/docs/authentication.md +129 -0
  7. fastapi_viewsets-1.5.2/docs/changelog.md +37 -0
  8. fastapi_viewsets-1.5.2/docs/comparison.md +67 -0
  9. fastapi_viewsets-1.5.2/docs/custom-routes.md +114 -0
  10. fastapi_viewsets-1.5.2/docs/eager-loading.md +77 -0
  11. fastapi_viewsets-1.5.2/docs/getting-started.md +91 -0
  12. fastapi_viewsets-1.5.2/docs/index.md +130 -0
  13. fastapi_viewsets-1.5.2/docs/orm-adapters.md +262 -0
  14. fastapi_viewsets-1.5.2/docs/overrides.md +161 -0
  15. fastapi_viewsets-1.5.2/docs/pagination-filtering.md +145 -0
  16. fastapi_viewsets-1.5.2/docs/quickstart-async.md +115 -0
  17. fastapi_viewsets-1.5.2/docs/quickstart-sync.md +152 -0
  18. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/__init__.py +11 -5
  19. fastapi_viewsets-1.5.2/fastapi_viewsets/_register.py +245 -0
  20. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/async_base.py +12 -6
  21. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/peewee_adapter.py +12 -11
  22. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/sqlalchemy_adapter.py +46 -30
  23. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/tortoise_adapter.py +13 -11
  24. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets.egg-info/PKG-INFO +68 -14
  25. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets.egg-info/SOURCES.txt +23 -1
  26. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets.egg-info/requires.txt +3 -3
  27. fastapi_viewsets-1.5.2/mkdocs.yml +115 -0
  28. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/pyproject.toml +4 -4
  29. fastapi_viewsets-1.5.2/pytest.ini +22 -0
  30. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/setup.cfg +0 -1
  31. fastapi_viewsets-1.5.2/tests/__init__.py +2 -0
  32. fastapi_viewsets-1.5.2/tests/conftest.py +210 -0
  33. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_async_utils.py +10 -10
  34. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_edge_cases.py +11 -13
  35. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_error_handling.py +1 -1
  36. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_exception_handling.py +3 -3
  37. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_integration.py +1 -1
  38. fastapi_viewsets-1.5.2/tests/test_quickstarts.py +98 -0
  39. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_utils.py +9 -9
  40. fastapi_viewsets-1.5.2/tests/test_write_path_regressions.py +447 -0
  41. fastapi_viewsets-1.5.2/tests/tortoise_models.py +28 -0
  42. fastapi_viewsets-1.5.0/fastapi_viewsets/_register.py +0 -133
  43. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/LICENSE +0 -0
  44. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/_compat.py +0 -0
  45. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/async_utils.py +0 -0
  46. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/constants.py +0 -0
  47. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/db_conf.py +0 -0
  48. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/filtering.py +0 -0
  49. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/__init__.py +0 -0
  50. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/base.py +0 -0
  51. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/orm/factory.py +0 -0
  52. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/serializer_utils.py +0 -0
  53. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets/utils.py +0 -0
  54. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets.egg-info/dependency_links.txt +0 -0
  55. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/fastapi_viewsets.egg-info/top_level.txt +0 -0
  56. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/setup.py +0 -0
  57. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_adapter_methods_coverage.py +0 -0
  58. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_async_base_viewset.py +0 -0
  59. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_async_driver_missing.py +0 -0
  60. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_backward_compatibility.py +0 -0
  61. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_base_viewset.py +0 -0
  62. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_coverage_gaps.py +0 -0
  63. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_db_conf.py +0 -0
  64. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_db_conf_extended.py +0 -0
  65. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_missing_coverage.py +0 -0
  66. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_orm_adapters.py +0 -0
  67. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_orm_adapters_extended.py +0 -0
  68. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_search_ordering.py +0 -0
  69. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_select_prefetch_related.py +0 -0
  70. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_tortoise_lifecycle.py +0 -0
  71. {fastapi_viewsets-1.5.0 → fastapi_viewsets-1.5.2}/tests/test_viewsets_with_adapters.py +0 -0
@@ -0,0 +1,3 @@
1
+ include pytest.ini mkdocs.yml
2
+ recursive-include tests *.py
3
+ recursive-include docs *.md *.css
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi_viewsets
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -25,12 +25,10 @@ Requires-Python: >=3.9
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE
27
27
  Requires-Dist: fastapi>=0.110.0
28
- Requires-Dist: uvicorn>=0.17.6
29
- Requires-Dist: SQLAlchemy>=1.4.36
30
28
  Requires-Dist: pydantic<3,>=2.5
31
29
  Requires-Dist: python-dotenv>=0.19.0
32
30
  Provides-Extra: sqlalchemy
33
- Requires-Dist: SQLAlchemy>=1.4.36; extra == "sqlalchemy"
31
+ Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "sqlalchemy"
34
32
  Provides-Extra: tortoise
35
33
  Requires-Dist: tortoise-orm<1.0,>=0.20.0; extra == "tortoise"
36
34
  Requires-Dist: asyncpg>=0.28.0; extra == "tortoise"
@@ -43,6 +41,8 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "test"
43
41
  Requires-Dist: httpx>=0.24.0; extra == "test"
44
42
  Requires-Dist: faker>=18.0.0; extra == "test"
45
43
  Requires-Dist: aiosqlite>=0.19.0; extra == "test"
44
+ Requires-Dist: uvicorn>=0.17.6; extra == "test"
45
+ Requires-Dist: SQLAlchemy[asyncio]>=2.0.0; extra == "test"
46
46
  Provides-Extra: lint
47
47
  Requires-Dist: ruff>=0.5; extra == "lint"
48
48
  Requires-Dist: black>=24; extra == "lint"
@@ -95,15 +95,21 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
95
95
  pip install fastapi-viewsets
96
96
  ```
97
97
 
98
- Optional extras (see `setup.py`):
98
+ The base install is ORM-agnostic: it pulls in only FastAPI, Pydantic and
99
+ python-dotenv. Pick your ORM via an extra (see `pyproject.toml`):
99
100
 
100
101
  ```bash
101
- pip install "fastapi-viewsets[sqlalchemy]"
102
+ pip install "fastapi-viewsets[sqlalchemy]" # SQLAlchemy 2.x, incl. asyncio support (greenlet)
102
103
  pip install "fastapi-viewsets[tortoise]"
103
104
  pip install "fastapi-viewsets[peewee]"
104
105
  pip install "fastapi-viewsets[test]" # pytest, httpx, coverage, etc.
105
106
  ```
106
107
 
108
+ For a local SQLite app, start with the
109
+ [sync quickstart](#quickstart-sqlalchemy-sync); no separate database driver is needed.
110
+ To execute the quickstart's `python main.py` (or serve any app) you also
111
+ need an ASGI server, e.g. `pip install uvicorn`.
112
+
107
113
  For async SQLAlchemy you still need a driver such as `aiosqlite`, `asyncpg`, or `aiomysql` alongside your database URL.
108
114
 
109
115
  ## Database connection examples
@@ -432,6 +438,9 @@ an async-capable URL and use the lazy helpers from `db_conf`. The
432
438
  package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
433
439
  `postgresql://` to `postgresql+asyncpg://`, etc.
434
440
 
441
+ Save the following as `main.py` in an empty folder, then run
442
+ `uvicorn main:app --reload`. Open `http://127.0.0.1:8000/docs` to try the API.
443
+
435
444
  ```python
436
445
  from contextlib import asynccontextmanager
437
446
 
@@ -492,10 +501,23 @@ Notes:
492
501
  `model_config = ConfigDict(from_attributes=True)` instead of the v1
493
502
  `class Config: orm_mode = True`.
494
503
  - `PATCH` uses `model_dump(exclude_unset=True)` internally, so unset
495
- fields are no longer overwritten with defaults.
504
+ fields are no longer overwritten with defaults. Since v1.5.2 the PATCH
505
+ body is validated against an auto-generated all-optional variant of
506
+ your `response_model`, so partial bodies pass validation even when the
507
+ schema has required fields, and an explicit JSON `null` clears a
508
+ nullable column.
509
+ - `PUT` replaces exactly the fields present in the payload; model
510
+ columns that are not part of the `response_model` are left untouched.
511
+ - `POST` never sends an explicit `NULL` primary key to the database —
512
+ `id: Optional[int] = None` in the schema is safe on every adapter.
513
+ - Integrity violations (e.g. duplicate unique values) return
514
+ `409 Conflict` with a sanitized message; raw SQL and driver internals
515
+ are never exposed in the response body.
496
516
  - If the async driver (`aiosqlite` / `asyncpg` / `aiomysql`) is not
497
517
  installed, sync usage still works — only `get_async_session()` raises
498
- a helpful `RuntimeError`.
518
+ a helpful `RuntimeError`. The `sqlalchemy` extra installs
519
+ `SQLAlchemy[asyncio]`, which already includes `greenlet` for async
520
+ sessions.
499
521
 
500
522
  ## Overriding `list` and `create_element` (custom LIST and POST)
501
523
 
@@ -503,8 +525,11 @@ Every CRUD handler is a regular method, so subclassing the viewset is
503
525
  the canonical way to add filtering, ordering, validation, conflict
504
526
  handling, and so on. The example below subclasses `AsyncBaseViewset`
505
527
  and overrides both `list` (case-insensitive search + simple ordering)
506
- and `create_element` (input normalization + map `IntegrityError` to
507
- 409).
528
+ and `create_element` (input normalization + custom conflict message).
529
+
530
+ > Since v1.5.2 the adapters themselves map `IntegrityError` to
531
+ > `409 Conflict`; override `create_element` only when you need a custom
532
+ > error payload or extra normalization.
508
533
 
509
534
  ```python
510
535
  from typing import List, Optional
@@ -670,7 +695,7 @@ app.include_router(router)
670
695
 
671
696
  ## Pagination, filtering, ordering
672
697
 
673
- **Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route.
698
+ **Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route. Defaults are `limit=10`, `offset=0`; negative values are rejected with `422`, and `limit` is capped at `10000`.
674
699
 
675
700
  ```python
676
701
  from fastapi_viewsets import BaseViewset
@@ -731,6 +756,34 @@ class ItemsWithStats(BaseViewset):
731
756
 
732
757
  ## What is new
733
758
 
759
+ ### v1.5.2
760
+
761
+ Bugfix release for the CRUD write paths (see
762
+ [RELEASE_1.5.2.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.5.2.md)):
763
+
764
+ - **Multi-viewset apps fixed**: `register()` no longer leaks one viewset's
765
+ body schema into other viewsets — every `POST`/`PUT`/`PATCH` endpoint
766
+ now validates against its own `response_model`.
767
+ - **PATCH is truly partial**: the PATCH body is validated against an
768
+ auto-generated all-optional variant of the response schema, and an
769
+ explicit JSON `null` clears a nullable column.
770
+ - **PUT no longer nulls columns** that are absent from the Pydantic
771
+ schema.
772
+ - **Integrity violations return `409 Conflict`** with a sanitized message
773
+ instead of `400` with raw SQL/driver internals.
774
+ - **Create never passes an explicit `NULL` primary key** — fixes Tortoise
775
+ `POST` and PostgreSQL inserts with `id: Optional[int] = None` schemas.
776
+ - **Pagination validated**: negative `limit`/`offset` rejected with `422`.
777
+ - **Filters advertised in OpenAPI**: whitelisted `ListConfig.filters`
778
+ fields and their `__op` variants show up in Swagger UI.
779
+ - **Leaner dependencies**: the base install no longer requires SQLAlchemy
780
+ or uvicorn; the `sqlalchemy` extra installs `SQLAlchemy[asyncio]`
781
+ (includes `greenlet`).
782
+
783
+ Earlier releases: v1.5.0 (server-side `search`, declarative ordering and
784
+ filters — [RELEASE_1.5.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.5.0.md)),
785
+ v1.4.0 ([RELEASE_1.4.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.4.0.md)).
786
+
734
787
  ### v1.3.0
735
788
 
736
789
  - **Declarative eager loading** via `RelatedConfig` inside Pydantic schemas.
@@ -746,9 +799,9 @@ class ItemsWithStats(BaseViewset):
746
799
  - Internal `register()` deduplicated between sync and async viewsets via a shared mixin.
747
800
  - PEP 621 `pyproject.toml`, `python_requires>=3.9`, FastAPI `>=0.110`, ruff/black/mypy preconfigured.
748
801
 
749
- 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.
802
+ Previous release: [v1.1.0](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.
750
803
 
751
- 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).
804
+ Details: [RELEASE_NOTES.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md), [RELEASE_1.2.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.2.0.md), [RELEASE_1.1.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md).
752
805
 
753
806
  ## Roadmap (planned)
754
807
 
@@ -773,6 +826,7 @@ Released: server-side `search` (v1.5.0), declarative ordering and advanced filte
773
826
  From the repository root (see `pytest.ini`):
774
827
 
775
828
  ```bash
829
+ python -m pip install -e ".[test]"
776
830
  pytest
777
831
  ```
778
832
 
@@ -784,7 +838,7 @@ See [open issues](https://github.com/svalench/fastapi_viewsets/issues) to propos
784
838
 
785
839
  ## License
786
840
 
787
- Distributed under the MIT License. See [LICENSE](LICENSE).
841
+ Distributed under the MIT License. See [LICENSE](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE).
788
842
 
789
843
  ## Author
790
844
 
@@ -40,15 +40,21 @@ Django REST Framework-style ViewSets for FastAPI — auto-generate CRUD endpoint
40
40
  pip install fastapi-viewsets
41
41
  ```
42
42
 
43
- Optional extras (see `setup.py`):
43
+ The base install is ORM-agnostic: it pulls in only FastAPI, Pydantic and
44
+ python-dotenv. Pick your ORM via an extra (see `pyproject.toml`):
44
45
 
45
46
  ```bash
46
- pip install "fastapi-viewsets[sqlalchemy]"
47
+ pip install "fastapi-viewsets[sqlalchemy]" # SQLAlchemy 2.x, incl. asyncio support (greenlet)
47
48
  pip install "fastapi-viewsets[tortoise]"
48
49
  pip install "fastapi-viewsets[peewee]"
49
50
  pip install "fastapi-viewsets[test]" # pytest, httpx, coverage, etc.
50
51
  ```
51
52
 
53
+ For a local SQLite app, start with the
54
+ [sync quickstart](#quickstart-sqlalchemy-sync); no separate database driver is needed.
55
+ To execute the quickstart's `python main.py` (or serve any app) you also
56
+ need an ASGI server, e.g. `pip install uvicorn`.
57
+
52
58
  For async SQLAlchemy you still need a driver such as `aiosqlite`, `asyncpg`, or `aiomysql` alongside your database URL.
53
59
 
54
60
  ## Database connection examples
@@ -377,6 +383,9 @@ an async-capable URL and use the lazy helpers from `db_conf`. The
377
383
  package auto-converts `sqlite://` to `sqlite+aiosqlite://`,
378
384
  `postgresql://` to `postgresql+asyncpg://`, etc.
379
385
 
386
+ Save the following as `main.py` in an empty folder, then run
387
+ `uvicorn main:app --reload`. Open `http://127.0.0.1:8000/docs` to try the API.
388
+
380
389
  ```python
381
390
  from contextlib import asynccontextmanager
382
391
 
@@ -437,10 +446,23 @@ Notes:
437
446
  `model_config = ConfigDict(from_attributes=True)` instead of the v1
438
447
  `class Config: orm_mode = True`.
439
448
  - `PATCH` uses `model_dump(exclude_unset=True)` internally, so unset
440
- fields are no longer overwritten with defaults.
449
+ fields are no longer overwritten with defaults. Since v1.5.2 the PATCH
450
+ body is validated against an auto-generated all-optional variant of
451
+ your `response_model`, so partial bodies pass validation even when the
452
+ schema has required fields, and an explicit JSON `null` clears a
453
+ nullable column.
454
+ - `PUT` replaces exactly the fields present in the payload; model
455
+ columns that are not part of the `response_model` are left untouched.
456
+ - `POST` never sends an explicit `NULL` primary key to the database —
457
+ `id: Optional[int] = None` in the schema is safe on every adapter.
458
+ - Integrity violations (e.g. duplicate unique values) return
459
+ `409 Conflict` with a sanitized message; raw SQL and driver internals
460
+ are never exposed in the response body.
441
461
  - If the async driver (`aiosqlite` / `asyncpg` / `aiomysql`) is not
442
462
  installed, sync usage still works — only `get_async_session()` raises
443
- a helpful `RuntimeError`.
463
+ a helpful `RuntimeError`. The `sqlalchemy` extra installs
464
+ `SQLAlchemy[asyncio]`, which already includes `greenlet` for async
465
+ sessions.
444
466
 
445
467
  ## Overriding `list` and `create_element` (custom LIST and POST)
446
468
 
@@ -448,8 +470,11 @@ Every CRUD handler is a regular method, so subclassing the viewset is
448
470
  the canonical way to add filtering, ordering, validation, conflict
449
471
  handling, and so on. The example below subclasses `AsyncBaseViewset`
450
472
  and overrides both `list` (case-insensitive search + simple ordering)
451
- and `create_element` (input normalization + map `IntegrityError` to
452
- 409).
473
+ and `create_element` (input normalization + custom conflict message).
474
+
475
+ > Since v1.5.2 the adapters themselves map `IntegrityError` to
476
+ > `409 Conflict`; override `create_element` only when you need a custom
477
+ > error payload or extra normalization.
453
478
 
454
479
  ```python
455
480
  from typing import List, Optional
@@ -615,7 +640,7 @@ app.include_router(router)
615
640
 
616
641
  ## Pagination, filtering, ordering
617
642
 
618
- **Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route.
643
+ **Pagination** — `BaseViewset.list` maps `limit` and `offset` to query parameters on the LIST route. Defaults are `limit=10`, `offset=0`; negative values are rejected with `422`, and `limit` is capped at `10000`.
619
644
 
620
645
  ```python
621
646
  from fastapi_viewsets import BaseViewset
@@ -676,6 +701,34 @@ class ItemsWithStats(BaseViewset):
676
701
 
677
702
  ## What is new
678
703
 
704
+ ### v1.5.2
705
+
706
+ Bugfix release for the CRUD write paths (see
707
+ [RELEASE_1.5.2.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.5.2.md)):
708
+
709
+ - **Multi-viewset apps fixed**: `register()` no longer leaks one viewset's
710
+ body schema into other viewsets — every `POST`/`PUT`/`PATCH` endpoint
711
+ now validates against its own `response_model`.
712
+ - **PATCH is truly partial**: the PATCH body is validated against an
713
+ auto-generated all-optional variant of the response schema, and an
714
+ explicit JSON `null` clears a nullable column.
715
+ - **PUT no longer nulls columns** that are absent from the Pydantic
716
+ schema.
717
+ - **Integrity violations return `409 Conflict`** with a sanitized message
718
+ instead of `400` with raw SQL/driver internals.
719
+ - **Create never passes an explicit `NULL` primary key** — fixes Tortoise
720
+ `POST` and PostgreSQL inserts with `id: Optional[int] = None` schemas.
721
+ - **Pagination validated**: negative `limit`/`offset` rejected with `422`.
722
+ - **Filters advertised in OpenAPI**: whitelisted `ListConfig.filters`
723
+ fields and their `__op` variants show up in Swagger UI.
724
+ - **Leaner dependencies**: the base install no longer requires SQLAlchemy
725
+ or uvicorn; the `sqlalchemy` extra installs `SQLAlchemy[asyncio]`
726
+ (includes `greenlet`).
727
+
728
+ Earlier releases: v1.5.0 (server-side `search`, declarative ordering and
729
+ filters — [RELEASE_1.5.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.5.0.md)),
730
+ v1.4.0 ([RELEASE_1.4.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.4.0.md)).
731
+
679
732
  ### v1.3.0
680
733
 
681
734
  - **Declarative eager loading** via `RelatedConfig` inside Pydantic schemas.
@@ -691,9 +744,9 @@ class ItemsWithStats(BaseViewset):
691
744
  - Internal `register()` deduplicated between sync and async viewsets via a shared mixin.
692
745
  - PEP 621 `pyproject.toml`, `python_requires>=3.9`, FastAPI `>=0.110`, ruff/black/mypy preconfigured.
693
746
 
694
- 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.
747
+ Previous release: [v1.1.0](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md) introduced multi-ORM support via adapters (SQLAlchemy default, optional Tortoise and Peewee), `ORMFactory` and environment-driven `ORM_TYPE` configuration.
695
748
 
696
- 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).
749
+ Details: [RELEASE_NOTES.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_NOTES.md), [RELEASE_1.2.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.2.0.md), [RELEASE_1.1.0.md](https://github.com/svalench/fastapi_viewsets/blob/master/RELEASE_1.1.0.md).
697
750
 
698
751
  ## Roadmap (planned)
699
752
 
@@ -718,6 +771,7 @@ Released: server-side `search` (v1.5.0), declarative ordering and advanced filte
718
771
  From the repository root (see `pytest.ini`):
719
772
 
720
773
  ```bash
774
+ python -m pip install -e ".[test]"
721
775
  pytest
722
776
  ```
723
777
 
@@ -729,7 +783,7 @@ See [open issues](https://github.com/svalench/fastapi_viewsets/issues) to propos
729
783
 
730
784
  ## License
731
785
 
732
- Distributed under the MIT License. See [LICENSE](LICENSE).
786
+ Distributed under the MIT License. See [LICENSE](https://github.com/svalench/fastapi_viewsets/blob/master/LICENSE).
733
787
 
734
788
  ## Author
735
789
 
@@ -0,0 +1,311 @@
1
+ # API Reference
2
+
3
+ ## `BaseViewset`
4
+
5
+ ```python
6
+ from fastapi_viewsets import BaseViewset
7
+ ```
8
+
9
+ Synchronous CRUD viewset. Subclasses `APIRouter` and `_RegisterMixin`. Provides `LIST`, `GET`, `POST`, `PUT`, `PATCH`, and `DELETE` endpoints generated from an ORM model and a Pydantic response model.
10
+
11
+ ### Constructor
12
+
13
+ ```python
14
+ BaseViewset(
15
+ *,
16
+ allowed_methods: Optional[List[str]] = None,
17
+ endpoint: Optional[str] = None,
18
+ model: Optional[Type[ModelType]] = None,
19
+ db_session: Optional[Callable[[], Any]] = None,
20
+ response_model: Optional[Type[BaseModel]] = None,
21
+ orm_adapter: Optional[BaseORMAdapter] = None,
22
+ **kwargs, # forwarded to APIRouter
23
+ )
24
+ ```
25
+
26
+ | Parameter | Type | Default | Description |
27
+ | --- | --- | --- | --- |
28
+ | `allowed_methods` | `Optional[List[str]]` | `None` | Override of `ALLOWED_METHODS` |
29
+ | `endpoint` | `Optional[str]` | `None` | Base endpoint path, e.g. `"/user"` |
30
+ | `model` | `Optional[Type]` | `None` | ORM model class |
31
+ | `db_session` | `Optional[Callable]` | `None` | Database session factory function |
32
+ | `response_model` | `Optional[Type[BaseModel]]` | `None` | Pydantic schema for request/response bodies |
33
+ | `orm_adapter` | `Optional[BaseORMAdapter]` | `None` | ORM adapter; resolved from config when omitted |
34
+ | `**kwargs` | — | — | Forwarded to `fastapi.APIRouter` (e.g. `tags`, `prefix`, `dependencies`) |
35
+
36
+ ### CRUD handlers
37
+
38
+ #### `list()`
39
+
40
+ ```python
41
+ def list(
42
+ self,
43
+ limit: Optional[int] = 10,
44
+ offset: Optional[int] = 0,
45
+ search: Optional[str] = None,
46
+ token: str = Depends(_noop_dependency),
47
+ ) -> List[ResponseModelType]
48
+ ```
49
+
50
+ List items with `limit`/`offset` pagination. Returns a list of `response_model` instances.
51
+
52
+ !!! warning "`search` parameter is reserved"
53
+
54
+ The `search` parameter is accepted by `list()` and appears in the
55
+ OpenAPI schema, but ORM adapters currently ignore it. Server-side
56
+ search is planned for v1.4. Until then, override `list()` in a
57
+ subclass to implement filtering — see
58
+ [Pagination & Filtering](pagination-filtering.md).
59
+
60
+ #### `get_element()`
61
+
62
+ ```python
63
+ def get_element(
64
+ self,
65
+ id: Union[int, str],
66
+ token: str = Depends(_noop_dependency),
67
+ ) -> ResponseModelType
68
+ ```
69
+
70
+ Retrieve a single item by ID. Raises `404` if `id` is empty or `None`.
71
+
72
+ #### `create_element()`
73
+
74
+ ```python
75
+ def create_element(
76
+ self,
77
+ item: ResponseModelType = Body(...),
78
+ token: str = Depends(_noop_dependency),
79
+ ) -> ResponseModelType
80
+ ```
81
+
82
+ Create a new item from the request body.
83
+
84
+ #### `update_element()`
85
+
86
+ ```python
87
+ def update_element(
88
+ self,
89
+ id: Union[int, str],
90
+ item: ResponseModelType = Body(...),
91
+ token: str = Depends(_noop_dependency),
92
+ partial: bool = False,
93
+ ) -> ResponseModelType
94
+ ```
95
+
96
+ Update an existing item. `partial=True` (used by `PATCH`) only writes fields the client explicitly set, preserving correct PATCH semantics under Pydantic v2.
97
+
98
+ #### `delete_element()`
99
+
100
+ ```python
101
+ def delete_element(
102
+ self,
103
+ id: Union[int, str],
104
+ token: str = Depends(_noop_dependency),
105
+ ) -> Dict[str, Union[bool, str]]
106
+ ```
107
+
108
+ Delete an item by ID. Returns `{"status": True, "text": "successfully deleted"}` or `{"status": False, "text": "deletion failed"}`.
109
+
110
+ ---
111
+
112
+ ## `AsyncBaseViewset`
113
+
114
+ ```python
115
+ from fastapi_viewsets import AsyncBaseViewset
116
+ ```
117
+
118
+ Asynchronous CRUD viewset. Mirrors `BaseViewset` but every CRUD handler is `async`, backed by an async-capable ORM adapter (SQLAlchemy `AsyncSession` or Tortoise ORM).
119
+
120
+ ### Constructor
121
+
122
+ Same parameters as `BaseViewset`, but `db_session` should be an async session factory (`Callable[[], AsyncSession]`).
123
+
124
+ ### CRUD handlers
125
+
126
+ Identical signatures to `BaseViewset`, but `async def`:
127
+
128
+ - `async def list(...)`
129
+ - `async def get_element(...)`
130
+ - `async def create_element(...)`
131
+ - `async def update_element(...)`
132
+ - `async def delete_element(...)`
133
+
134
+ ---
135
+
136
+ ## `register()`
137
+
138
+ ```python
139
+ def register(
140
+ self,
141
+ methods: Optional[List[str]] = None,
142
+ oauth_protect: Optional[OAuth2PasswordBearer] = None,
143
+ protected_methods: Optional[List[str]] = None,
144
+ ) -> None
145
+ ```
146
+
147
+ Register CRUD endpoints on the router.
148
+
149
+ | Parameter | Type | Default | Description |
150
+ | --- | --- | --- | --- |
151
+ | `methods` | `Optional[List[str]]` | `None` (all) | Logical methods to register |
152
+ | `oauth_protect` | `Optional[OAuth2PasswordBearer]` | `None` | OAuth2 dependency for protected operations |
153
+ | `protected_methods` | `Optional[List[str]]` | `None` | Subset of `methods` requiring the bearer token |
154
+
155
+ ### Allowed methods
156
+
157
+ ```python
158
+ ALLOWED_METHODS = ["LIST", "POST", "GET", "PUT", "PATCH", "DELETE"]
159
+ ```
160
+
161
+ ### Method-to-route mapping
162
+
163
+ | Logical method | HTTP method | Path | Handler | Response |
164
+ | --- | --- | --- | --- | --- |
165
+ | `LIST` | `GET` | `/` | `list` | `List[response_model]` |
166
+ | `GET` | `GET` | `/{id}` | `get_element` | `response_model` |
167
+ | `POST` | `POST` | `/` | `create_element` | `response_model` |
168
+ | `PUT` | `PUT` | `/{id}` | `update_element` (`partial=False`) | `response_model` |
169
+ | `PATCH` | `PATCH` | `/{id}` | `update_element` (`partial=True`) | `response_model` |
170
+ | `DELETE` | `DELETE` | `/{id}` | `delete_element` | `None` (raw dict) |
171
+
172
+ ---
173
+
174
+ ## `ORMFactory`
175
+
176
+ ```python
177
+ from fastapi_viewsets.orm.factory import ORMFactory
178
+ ```
179
+
180
+ Central entry point for adapter resolution.
181
+
182
+ ### Methods
183
+
184
+ | Method | Returns | Description |
185
+ | --- | --- | --- |
186
+ | `get_default_adapter()` | `BaseORMAdapter` | Returns the cached singleton adapter for the current `ORM_TYPE` |
187
+ | `create_adapter(orm_type, config)` | `BaseORMAdapter` | Creates a new adapter instance from a dict config |
188
+ | `register_adapter(orm_type, adapter_class)` | `None` | Registers a custom adapter class (e.g. for a new ORM) |
189
+ | `get_adapter_from_env()` | `BaseORMAdapter` | Builds an adapter from environment variables (used internally by `get_default_adapter`) |
190
+ | `reset_default_adapter()` | `None` | Clears the cached singleton; the next `get_default_adapter()` call rebuilds from env. Useful in tests and hot-reload scenarios. |
191
+
192
+ ---
193
+
194
+ ## `BaseORMAdapter`
195
+
196
+ ```python
197
+ from fastapi_viewsets.orm.base import BaseORMAdapter
198
+ ```
199
+
200
+ Abstract base class for ORM adapters. All adapters (SQLAlchemy, Tortoise, Peewee) implement this interface.
201
+
202
+ ### Abstract methods
203
+
204
+ | Method | Sync/Async | Description |
205
+ | --- | --- | --- |
206
+ | `get_session()` | Sync | Get a synchronous database session |
207
+ | `get_async_session()` | Sync | Get an async database session |
208
+ | `get_base()` | Sync | Get the base class for ORM models |
209
+ | `get_list_queryset(model, db_session, limit, offset, select_related, prefetch_related)` | Sync | Get a paginated list of model instances |
210
+ | `get_list_queryset_async(model, db_session, limit, offset, select_related, prefetch_related)` | Async | Async version of `get_list_queryset` |
211
+ | `get_element_by_id(model, db_session, id, select_related, prefetch_related)` | Sync | Get a single element by ID |
212
+ | `get_element_by_id_async(model, db_session, id, select_related, prefetch_related)` | Async | Async version of `get_element_by_id` |
213
+ | `create_element(model, db_session, data)` | Sync | Create a new element |
214
+ | `create_element_async(model, db_session, data)` | Async | Async version of `create_element` |
215
+ | `update_element(model, db_session, id, data, partial)` | Sync | Update an element (`partial=True` for PATCH) |
216
+ | `update_element_async(model, db_session, id, data, partial)` | Async | Async version of `update_element` |
217
+ | `delete_element(model, db_session, id)` | Sync | Delete an element by ID |
218
+ | `delete_element_async(model, db_session, id)` | Async | Async version of `delete_element` |
219
+ | `get_model_columns(model)` | Sync | Get column information for a model |
220
+
221
+ ---
222
+
223
+ ## `db_conf` module
224
+
225
+ ```python
226
+ from fastapi_viewsets.db_conf import (
227
+ ORM_TYPE,
228
+ SQLALCHEMY_DATABASE_URL,
229
+ get_orm_adapter,
230
+ # Lazy SQLAlchemy globals:
231
+ engine,
232
+ Base,
233
+ SessionLocal,
234
+ db_session,
235
+ get_session,
236
+ async_engine,
237
+ AsyncSessionLocal,
238
+ get_async_session,
239
+ )
240
+ ```
241
+
242
+ ### Environment variables
243
+
244
+ | Variable | Default | Description |
245
+ | --- | --- | --- |
246
+ | `ORM_TYPE` | `sqlalchemy` | Active ORM: `sqlalchemy`, `tortoise`, or `peewee` |
247
+ | `DATABASE_URL` | `sqlite:///<cwd>/base.db` | Generic database URL (fallback) |
248
+ | `SQLALCHEMY_DATABASE_URL` | — | SQLAlchemy-specific URL |
249
+ | `SQLALCHEMY_ASYNC_DATABASE_URL` | Auto-derived | Explicit async URL (overrides auto-conversion) |
250
+ | `TORTOISE_DATABASE_URL` | — | Tortoise database URL |
251
+ | `TORTOISE_MODELS` | — | JSON list of model modules, e.g. `["app.models"]` |
252
+ | `TORTOISE_APP_LABEL` | `models` | Tortoise app label |
253
+ | `PEEWEE_DATABASE_URL` | — | Peewee database URL |
254
+
255
+ ### Lazy resolution
256
+
257
+ SQLAlchemy globals (`engine`, `Base`, `get_session`, `get_async_session`, etc.) are resolved lazily on first access. Importing the package does not create engines unless they are needed, and works without async drivers installed.
258
+
259
+ ---
260
+
261
+ ## Serializer utilities
262
+
263
+ ```python
264
+ from fastapi_viewsets.serializer_utils import get_select_related, get_prefetch_related
265
+ ```
266
+
267
+ These helpers read eager-loading configuration from a Pydantic schema's inner `RelatedConfig` class. They are used internally by `get_list_queryset` and `get_element_by_id` (sync and async) when a `response_model` is provided.
268
+
269
+ | Function | Returns | Description |
270
+ | --- | --- | --- |
271
+ | `get_select_related(response_model)` | `List[str]` | Reads `RelatedConfig.select_related` (FK / many-to-one relations) |
272
+ | `get_prefetch_related(response_model)` | `List[str]` | Reads `RelatedConfig.prefetch_related` (collections / M2M relations) |
273
+
274
+ See [Eager Loading](eager-loading.md) for usage examples.
275
+
276
+ ---
277
+
278
+ ## Internal auth placeholder
279
+
280
+ The `token` parameter on every CRUD handler defaults to `Depends(_noop_dependency)`, where `_noop_dependency` is a private function that returns `None`. It is overridden when `oauth_protect` is passed to `register()`. The backward-compatible alias `butle` also points to this function.
281
+
282
+ You should not import `_noop_dependency` directly. If you override a handler and want to preserve the optional-auth behaviour, use `Depends(lambda: None)` or your own no-op dependency.
283
+
284
+ ---
285
+
286
+ ## Constants
287
+
288
+ ```python
289
+ from fastapi_viewsets.constants import ALLOWED_METHODS, MAP_METHODS
290
+ ```
291
+
292
+ ### `ALLOWED_METHODS`
293
+
294
+ ```python
295
+ ALLOWED_METHODS = ["LIST", "POST", "GET", "PUT", "PATCH", "DELETE"]
296
+ ```
297
+
298
+ ### `MAP_METHODS`
299
+
300
+ Maps logical method names to their route specifications:
301
+
302
+ ```python
303
+ MAP_METHODS = {
304
+ "GET": {"method": "get_element", "http_method": "GET", "path": "/{id}", "is_list": False},
305
+ "POST": {"method": "create_element", "http_method": "POST", "path": "", "is_list": False},
306
+ "PUT": {"method": "update_element", "http_method": "PUT", "path": "/{id}", "is_list": False},
307
+ "PATCH": {"method": "update_element", "http_method": "PATCH", "path": "/{id}", "is_list": False},
308
+ "DELETE": {"method": "delete_element", "http_method": "DELETE", "path": "/{id}", "is_list": False},
309
+ "LIST": {"method": "list", "http_method": "GET", "path": "", "is_list": True},
310
+ }
311
+ ```
@@ -0,0 +1,25 @@
1
+ /* fastapi-viewsets docs custom styles */
2
+
3
+ .md-header__title {
4
+ font-weight: 700;
5
+ }
6
+
7
+ .md-typeset__table td:not(:last-child),
8
+ .md-typeset__table th:not(:last-child) {
9
+ padding-right: 1.2rem;
10
+ }
11
+
12
+ .md-typeset .grid.cards > ul > li {
13
+ border: 1px solid var(--md-default-fg-color--lightest);
14
+ border-radius: 0.4rem;
15
+ transition: border-color 0.25s, box-shadow 0.25s;
16
+ }
17
+
18
+ .md-typeset .grid.cards > ul > li:hover {
19
+ border-color: var(--md-accent-fg-color);
20
+ box-shadow: 0 0 0.2rem var(--md-accent-fg-color);
21
+ }
22
+
23
+ .md-typeset code {
24
+ font-size: 0.85em;
25
+ }