dynamicforms-fastapi-viewsets 0.15.0__tar.gz → 0.15.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.
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/PKG-INFO +1 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/cursor.py +11 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/build_schema.py +4 -3
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/route_viewset.py +85 -4
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/endpoint_docs.py +14 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/changes.py +8 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/viewset.py +20 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/list_query.py +9 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mixins.py +22 -11
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/routing.py +25 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/pyproject.toml +1 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/pyproject.toml.orig +1 -1
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/LICENSE +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/README.md +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/action_configuration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/backends/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/backends/django_orm.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/collection_viewset.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/conf.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/context/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/context/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/context/auth/django.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/context/auth/jwt.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/context/auth/static.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/client.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/payload.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/celery_viewset/transport.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/conftest.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/query_model.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/decorators/sub_viewsets.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/error_namespace.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/error_responses.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/authorizers.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/conftest.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/delivery.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/event.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/lifecycle.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/redis.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/scopes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/sources.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/store.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/events/topics.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/exceptions.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/filters/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/filters/base.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/filters/declaration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/filters/operators.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/filters/registry.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/list_shapes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/cookies.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/idempotency.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/asgi.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/protocol.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/registry.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/mux_ws/transports.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/py.typed +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/response_classes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/save_state/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/save_state/save_state.py +0 -0
- {dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/fastapi_viewsets/save_state/serialize_state.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamicforms-fastapi-viewsets
|
|
3
|
-
Version: 0.15.
|
|
3
|
+
Version: 0.15.1
|
|
4
4
|
Summary: Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart.
|
|
5
5
|
Keywords: fastapi,viewsets,rest,dynamicforms,celery,django-rest-framework
|
|
6
6
|
Author: Jure Erznožnik
|
|
@@ -15,7 +15,7 @@ import json
|
|
|
15
15
|
from dataclasses import dataclass
|
|
16
16
|
from typing import Any, ClassVar, Generic, TYPE_CHECKING
|
|
17
17
|
|
|
18
|
-
from pydantic import BaseModel, TypeAdapter
|
|
18
|
+
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
|
19
19
|
from typing_extensions import TypeVar
|
|
20
20
|
|
|
21
21
|
from .error_namespace import _namespaced
|
|
@@ -157,14 +157,24 @@ class CursorPage(BaseModel, Generic[TItem]):
|
|
|
157
157
|
are the same anchors read inclusively, present whenever the page is non-empty, for polling an edge.
|
|
158
158
|
"""
|
|
159
159
|
|
|
160
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
161
|
+
|
|
160
162
|
results: list[TItem]
|
|
163
|
+
"""The records of the page."""
|
|
161
164
|
limit: int
|
|
165
|
+
"""Page size the page was read with."""
|
|
162
166
|
has_more: bool = False
|
|
167
|
+
"""Whether records follow the page."""
|
|
163
168
|
has_previous: bool = False
|
|
169
|
+
"""Whether records precede the page."""
|
|
164
170
|
next: str | None = None
|
|
171
|
+
"""Cursor of the records after the page; null when there are none."""
|
|
165
172
|
previous: str | None = None
|
|
173
|
+
"""Cursor of the records before the page; null when there are none."""
|
|
166
174
|
first: str | None = None
|
|
175
|
+
"""Cursor of the page's first record onwards (that record included); null for an empty page."""
|
|
167
176
|
last: str | None = None
|
|
177
|
+
"""Cursor of the page's last record backwards (that record included); null for an empty page."""
|
|
168
178
|
|
|
169
179
|
|
|
170
180
|
@dataclass(frozen=True)
|
|
@@ -7,8 +7,9 @@ from fastapi import APIRouter, FastAPI
|
|
|
7
7
|
from fastapi.routing import APIRoute
|
|
8
8
|
from starlette.responses import Response
|
|
9
9
|
|
|
10
|
-
from ..endpoint_docs import
|
|
10
|
+
from ..endpoint_docs import operation_docs_for, viewset_tags
|
|
11
11
|
from ..middleware import unwrap_viewset_result_type
|
|
12
|
+
from ..routing import SCHEMA_ENDPOINT_DOCS
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def _generic_args(annotation) -> tuple:
|
|
@@ -144,7 +145,7 @@ def build_schema(cls, base_path: str = "", default_tags=None, get_wrapper=None,
|
|
|
144
145
|
endpoint=schema,
|
|
145
146
|
tags=default_tags,
|
|
146
147
|
methods=["GET"],
|
|
147
|
-
|
|
148
|
+
**SCHEMA_ENDPOINT_DOCS,
|
|
148
149
|
)
|
|
149
150
|
class_router.add_api_route(**schema_route_kwargs)
|
|
150
151
|
|
|
@@ -179,7 +180,7 @@ def build_schema(cls, base_path: str = "", default_tags=None, get_wrapper=None,
|
|
|
179
180
|
route.endpoint.__name__,
|
|
180
181
|
getattr(endpoint_wrapper, "__shape_openapi_extra__", None) or route.openapi_extra,
|
|
181
182
|
),
|
|
182
|
-
**
|
|
183
|
+
**operation_docs_for(cls, route.endpoint.__name__),
|
|
183
184
|
)
|
|
184
185
|
)
|
|
185
186
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import inspect
|
|
2
3
|
import re
|
|
3
4
|
|
|
@@ -5,8 +6,8 @@ from dataclasses import dataclass
|
|
|
5
6
|
from functools import wraps
|
|
6
7
|
from typing import Annotated, get_args, get_origin, Literal, TypeVar
|
|
7
8
|
|
|
8
|
-
from fastapi import APIRouter, Depends, Header, Request, Response
|
|
9
|
-
from fastapi.params import FieldInfo
|
|
9
|
+
from fastapi import APIRouter, Depends, Header, Path, Request, Response
|
|
10
|
+
from fastapi.params import FieldInfo, Header as HeaderParam, Param
|
|
10
11
|
from pydantic import BaseModel
|
|
11
12
|
|
|
12
13
|
try:
|
|
@@ -20,11 +21,15 @@ from fastapi_viewsets.context import build_context, Context, get_shared_context
|
|
|
20
21
|
from fastapi_viewsets.endpoint_docs import (
|
|
21
22
|
DOCS_ATTRIBUTE,
|
|
22
23
|
docs_for,
|
|
24
|
+
operation_docs_for,
|
|
23
25
|
register_tag,
|
|
24
26
|
viewset_description,
|
|
25
27
|
)
|
|
26
28
|
from fastapi_viewsets.middleware import any_modifies_response_shape, Middleware, unwrap_viewset_result_type
|
|
27
|
-
from fastapi_viewsets.mixins import
|
|
29
|
+
from fastapi_viewsets.mixins import (
|
|
30
|
+
FilterParam,
|
|
31
|
+
T as ITEM_TYPE_VAR, # noqa: N811
|
|
32
|
+
)
|
|
28
33
|
from fastapi_viewsets.mux_ws import register_viewset, resolve_register_muxws, resolve_register_rest
|
|
29
34
|
from fastapi_viewsets.response_classes import with_error_responses
|
|
30
35
|
from fastapi_viewsets.routing import MUXWS, openapi_endpoint, REST, transport_route_class
|
|
@@ -172,6 +177,48 @@ def _with_security_scheme(run):
|
|
|
172
177
|
return dependency
|
|
173
178
|
|
|
174
179
|
|
|
180
|
+
PK_DESCRIPTION = "Primary key of the record."
|
|
181
|
+
"""The `pk` path parameter's description when the model's primary key field has none."""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _pk_description(model, pk_field_name: str | None) -> str:
|
|
185
|
+
"""The description of the `pk` path parameter: that of the model's primary key field, else `PK_DESCRIPTION`."""
|
|
186
|
+
fields = getattr(model, "model_fields", None) or {}
|
|
187
|
+
field = fields.get(pk_field_name) if pk_field_name else None
|
|
188
|
+
return field.description if field is not None and field.description else PK_DESCRIPTION
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _published_name(name: str, annotation) -> str | None:
|
|
192
|
+
"""The schema name of a path, query, header or cookie parameter; None for any other parameter."""
|
|
193
|
+
for metadata in getattr(annotation, "__metadata__", ()):
|
|
194
|
+
if isinstance(metadata, Param):
|
|
195
|
+
if metadata.alias:
|
|
196
|
+
return metadata.alias
|
|
197
|
+
if isinstance(metadata, HeaderParam) and metadata.convert_underscores:
|
|
198
|
+
return name.replace("_", "-")
|
|
199
|
+
return name
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _with_description(annotation, description: str):
|
|
204
|
+
"""`annotation` (`Annotated[X, Param]`) with the parameter's description replaced."""
|
|
205
|
+
metadata = []
|
|
206
|
+
for item in annotation.__metadata__:
|
|
207
|
+
if isinstance(item, Param):
|
|
208
|
+
item = copy.copy(item)
|
|
209
|
+
item.description = description
|
|
210
|
+
item._attributes_set = {**getattr(item, "_attributes_set", {}), "description": description}
|
|
211
|
+
metadata.append(item)
|
|
212
|
+
return Annotated[(annotation.__origin__, *metadata)]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _is_absent_model_param(annotation) -> bool:
|
|
216
|
+
"""Whether `annotation` is `Annotated[None, Query()]`: a filter parameter of a viewset declaring no filter."""
|
|
217
|
+
args = getattr(annotation, "__args__", None)
|
|
218
|
+
metadata = getattr(annotation, "__metadata__", None)
|
|
219
|
+
return bool(args and metadata) and args[0] in (None, type(None)) and any(isinstance(m, FieldInfo) for m in metadata)
|
|
220
|
+
|
|
221
|
+
|
|
175
222
|
def route_viewset(
|
|
176
223
|
router: APIRouter,
|
|
177
224
|
base_path: str,
|
|
@@ -276,6 +323,14 @@ def _route(
|
|
|
276
323
|
),
|
|
277
324
|
]
|
|
278
325
|
)
|
|
326
|
+
if name == "limit":
|
|
327
|
+
parameter = parameter.replace(
|
|
328
|
+
annotation=_with_description(
|
|
329
|
+
parameter.annotation,
|
|
330
|
+
f"Page size. Omit for {cls.default_page_size}; a larger value than {cls.max_page_size}"
|
|
331
|
+
f" returns {cls.max_page_size}.",
|
|
332
|
+
)
|
|
333
|
+
)
|
|
279
334
|
if name == "sort" and cls.default_sort is not None:
|
|
280
335
|
check_declared_sort(cls.default_sort, item_type)
|
|
281
336
|
parameter = parameter.replace(default=cls.default_sort)
|
|
@@ -286,6 +341,26 @@ def _route(
|
|
|
286
341
|
return sig.replace(parameters=params, return_annotation=response_model(allowed, item_type))
|
|
287
342
|
|
|
288
343
|
shape_openapi_extra = None
|
|
344
|
+
item_model = type_map.get(ITEM_TYPE_VAR)
|
|
345
|
+
|
|
346
|
+
def _describe_parameters(params: list[inspect.Parameter], descriptions: dict[str, str]) -> None:
|
|
347
|
+
"""Replaces the descriptions of `params` named in `descriptions` (`@endpoint_docs` `parameters`)."""
|
|
348
|
+
if not descriptions:
|
|
349
|
+
return
|
|
350
|
+
published = {}
|
|
351
|
+
for index, parameter in enumerate(params):
|
|
352
|
+
name = _published_name(parameter.name, parameter.annotation)
|
|
353
|
+
if name is not None:
|
|
354
|
+
published[name] = index
|
|
355
|
+
unknown = set(descriptions) - set(published)
|
|
356
|
+
if unknown:
|
|
357
|
+
raise ValueError(
|
|
358
|
+
f"@endpoint_docs on {cls.__name__} describes parameter(s) its endpoint does not have: "
|
|
359
|
+
f"{', '.join(sorted(unknown))}; it has {', '.join(sorted(published)) or 'none'}"
|
|
360
|
+
)
|
|
361
|
+
for name, description in descriptions.items():
|
|
362
|
+
index = published[name]
|
|
363
|
+
params[index] = params[index].replace(annotation=_with_description(params[index].annotation, description))
|
|
289
364
|
|
|
290
365
|
def get_wrapper(original_endpoint, route_path, route_methods):
|
|
291
366
|
nonlocal shape_openapi_extra
|
|
@@ -303,6 +378,10 @@ def _route(
|
|
|
303
378
|
continue
|
|
304
379
|
|
|
305
380
|
annotation = resolve_typevars(type_map, p.annotation)
|
|
381
|
+
if _is_absent_model_param(annotation):
|
|
382
|
+
continue
|
|
383
|
+
if name == "pk" and "{pk}" in route_path and _published_name(name, annotation) is None:
|
|
384
|
+
annotation = Annotated[annotation, Path(description=_pk_description(item_model, pk_field_name))]
|
|
306
385
|
new_p = p.replace(annotation=annotation)
|
|
307
386
|
|
|
308
387
|
# `context` (detected by name) never reaches FastAPI; lifecycle_runner builds and injects it.
|
|
@@ -335,6 +414,8 @@ def _route(
|
|
|
335
414
|
|
|
336
415
|
new_params.append(new_p)
|
|
337
416
|
|
|
417
|
+
_describe_parameters(new_params, docs_for(cls, original_endpoint.__name__).get("parameters", {}))
|
|
418
|
+
|
|
338
419
|
# Reserved: FastAPI injects the real Response, onto which lifecycle_runner applies headers/cookies.
|
|
339
420
|
new_params.append(inspect.Parameter("response", inspect.Parameter.KEYWORD_ONLY, annotation=Response))
|
|
340
421
|
|
|
@@ -461,7 +542,7 @@ def _route(
|
|
|
461
542
|
route, dependencies=dependencies, responses=with_error_responses(route.responses)
|
|
462
543
|
)
|
|
463
544
|
# Per-viewset wording from @endpoint_docs.
|
|
464
|
-
route_kwargs.update(
|
|
545
|
+
route_kwargs.update(operation_docs_for(cls, action_name))
|
|
465
546
|
if parent is not None and not route_kwargs.get("summary"):
|
|
466
547
|
# Prefix the mount, telling a sub-viewset's `List Items` apart from the parent's in its group.
|
|
467
548
|
default_summary = (route_kwargs.get("name") or action_name).replace("_", " ").title()
|
|
@@ -19,6 +19,8 @@ class EndpointDoc(TypedDict, total=False):
|
|
|
19
19
|
response_description: str
|
|
20
20
|
deprecated: bool
|
|
21
21
|
tags: list[str]
|
|
22
|
+
parameters: dict[str, str]
|
|
23
|
+
"""Descriptions of the endpoint's path, query and header parameters, by their name in the schema."""
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
def endpoint_docs(docs: dict[str, EndpointDoc]):
|
|
@@ -40,15 +42,26 @@ def endpoint_docs(docs: dict[str, EndpointDoc]):
|
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def docs_for(cls: type, action: str) -> dict[str, Any]:
|
|
43
|
-
"""
|
|
45
|
+
"""
|
|
46
|
+
The documentation of one action, merged along the MRO (subclass entries override; `parameters` merges
|
|
47
|
+
per parameter).
|
|
48
|
+
"""
|
|
44
49
|
merged: dict[str, Any] = {}
|
|
45
50
|
for base in reversed(cls.__mro__):
|
|
46
51
|
entry = base.__dict__.get(DOCS_ATTRIBUTE, {}).get(action)
|
|
47
52
|
if entry:
|
|
53
|
+
parameters = {**merged.get("parameters", {}), **entry.get("parameters", {})}
|
|
48
54
|
merged.update(entry)
|
|
55
|
+
if parameters:
|
|
56
|
+
merged["parameters"] = parameters
|
|
49
57
|
return merged
|
|
50
58
|
|
|
51
59
|
|
|
60
|
+
def operation_docs_for(cls: type, action: str) -> dict[str, Any]:
|
|
61
|
+
"""`docs_for` without `parameters`: the keyword arguments of `add_api_route`."""
|
|
62
|
+
return {key: value for key, value in docs_for(cls, action).items() if key != "parameters"}
|
|
63
|
+
|
|
64
|
+
|
|
52
65
|
# ---------------------------------------------------------------------------
|
|
53
66
|
# Viewset-level description
|
|
54
67
|
# ---------------------------------------------------------------------------
|
|
@@ -10,7 +10,7 @@ import logging
|
|
|
10
10
|
from collections.abc import Awaitable, Callable, Mapping
|
|
11
11
|
from typing import Any, Literal, TYPE_CHECKING
|
|
12
12
|
|
|
13
|
-
from pydantic import BaseModel
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
14
|
|
|
15
15
|
from ..action_configuration import resolve_action_configuration
|
|
16
16
|
from ..context import _Deferred, Context
|
|
@@ -48,7 +48,10 @@ _OPTIONS = frozenset({"topic"})
|
|
|
48
48
|
class Change(BaseModel):
|
|
49
49
|
"""One record an action touched, by its pk; a frontend reads the record through the viewset's retrieve."""
|
|
50
50
|
|
|
51
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
52
|
+
|
|
51
53
|
pk: Any
|
|
54
|
+
"""Primary key of the record."""
|
|
52
55
|
|
|
53
56
|
|
|
54
57
|
class RecordsChanged(Event):
|
|
@@ -56,8 +59,12 @@ class RecordsChanged(Event):
|
|
|
56
59
|
|
|
57
60
|
event_name = "records_changed"
|
|
58
61
|
|
|
62
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
63
|
+
|
|
59
64
|
op: Literal["created", "updated", "deleted"]
|
|
65
|
+
"""What the action did to the records."""
|
|
60
66
|
changes: list[Change]
|
|
67
|
+
"""The records the action touched."""
|
|
61
68
|
|
|
62
69
|
|
|
63
70
|
def routed_base_path(request: "Request") -> str | None:
|
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
from fastapi import APIRouter, Request
|
|
10
10
|
from pydantic import (
|
|
11
11
|
BaseModel,
|
|
12
|
+
ConfigDict,
|
|
12
13
|
Field,
|
|
13
14
|
GetJsonSchemaHandler,
|
|
14
15
|
model_serializer,
|
|
@@ -44,12 +45,17 @@ def _leave_out_none(data: Any, names: tuple[str, ...]) -> Any:
|
|
|
44
45
|
class TopicRequest(BaseModel):
|
|
45
46
|
"""The body of `subscribe` and `unsubscribe`."""
|
|
46
47
|
|
|
48
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
49
|
+
|
|
47
50
|
topic: str
|
|
51
|
+
"""The topic: a viewset's routed base path or a declared source's `name[:key]`."""
|
|
48
52
|
|
|
49
53
|
|
|
50
54
|
class SubscribeAnswer(BaseModel):
|
|
51
55
|
"""The answer of `subscribe`. `scope` is left out of the serialized answer when it is None."""
|
|
52
56
|
|
|
57
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
58
|
+
|
|
53
59
|
rev: int
|
|
54
60
|
"""The topic's current rev, 0 for a topic without events."""
|
|
55
61
|
scope: str | None = None
|
|
@@ -68,7 +74,10 @@ class SubscribeAnswer(BaseModel):
|
|
|
68
74
|
class PollRequest(BaseModel):
|
|
69
75
|
"""The body of `poll`: each topic mapped to the rev the caller holds, or None to start from now."""
|
|
70
76
|
|
|
77
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
78
|
+
|
|
71
79
|
cursors: dict[str, int | None]
|
|
80
|
+
"""The rev the caller holds per topic; null to start from the topic's current rev."""
|
|
72
81
|
scopes: dict[str, str] | None = None
|
|
73
82
|
"""The scope key the caller holds per topic; a cursor whose scope key differs from the server's answers
|
|
74
83
|
`events: null`. None trusts every cursor."""
|
|
@@ -77,13 +86,17 @@ class PollRequest(BaseModel):
|
|
|
77
86
|
class PolledEvent(BaseModel):
|
|
78
87
|
"""One event of a poll answer."""
|
|
79
88
|
|
|
89
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
90
|
+
|
|
80
91
|
rev: int
|
|
92
|
+
"""The event's rev in its topic."""
|
|
81
93
|
prev: int
|
|
82
94
|
"""The rev of the event before it in the answer; for the first event, the cursor."""
|
|
83
95
|
event: str
|
|
84
96
|
"""The event's `event_name`."""
|
|
85
97
|
version: int = Field(ge=1, description="The event's `event_version`: the version of the payload's shape.")
|
|
86
98
|
payload: Any = None
|
|
99
|
+
"""The event's fields."""
|
|
87
100
|
|
|
88
101
|
|
|
89
102
|
class TopicAnswer(BaseModel):
|
|
@@ -93,9 +106,14 @@ class TopicAnswer(BaseModel):
|
|
|
93
106
|
and `status` set. `error`, `status` and `scope` are left out when None.
|
|
94
107
|
"""
|
|
95
108
|
|
|
109
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
110
|
+
|
|
96
111
|
rev: int | None
|
|
112
|
+
"""The topic's current rev; null for a refused or invalid topic."""
|
|
97
113
|
events: list[PolledEvent] | None
|
|
114
|
+
"""The events after the cursor, oldest first; null when the log cannot answer the cursor or the topic is refused."""
|
|
98
115
|
error: ApiErrorBody | None = None
|
|
116
|
+
"""Why the topic is refused or invalid."""
|
|
99
117
|
status: int | None = None
|
|
100
118
|
"""The HTTP status of the refusal; set on every answer with `error`."""
|
|
101
119
|
scope: str | None = None
|
|
@@ -114,6 +132,8 @@ class TopicAnswer(BaseModel):
|
|
|
114
132
|
class PollAnswer(BaseModel):
|
|
115
133
|
"""The answer of `poll`."""
|
|
116
134
|
|
|
135
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
136
|
+
|
|
117
137
|
topics: dict[str, TopicAnswer]
|
|
118
138
|
"""One answer per cursor of the request, in the request's order."""
|
|
119
139
|
poll_after_ms: int
|
|
@@ -8,7 +8,7 @@ from collections.abc import AsyncIterable, AsyncIterator, Iterable
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from typing import Any, Generic, TypeVar
|
|
10
10
|
|
|
11
|
-
from pydantic import BaseModel
|
|
11
|
+
from pydantic import BaseModel, ConfigDict
|
|
12
12
|
|
|
13
13
|
TItem = TypeVar("TItem")
|
|
14
14
|
"""
|
|
@@ -61,12 +61,20 @@ class ListQuery:
|
|
|
61
61
|
class PaginatedList(BaseModel, Generic[TItem]):
|
|
62
62
|
"""One page of results; `count` is None when the source cannot be counted without draining it."""
|
|
63
63
|
|
|
64
|
+
model_config = ConfigDict(use_attribute_docstrings=True)
|
|
65
|
+
|
|
64
66
|
results: list[TItem]
|
|
67
|
+
"""The records of the page."""
|
|
65
68
|
offset: int
|
|
69
|
+
"""Number of records before the page."""
|
|
66
70
|
limit: int | None
|
|
71
|
+
"""Page size the page was read with."""
|
|
67
72
|
count: int | None = None
|
|
73
|
+
"""Number of records in the whole list; null when the source cannot be counted."""
|
|
68
74
|
has_more: bool = False
|
|
75
|
+
"""Whether records follow the page."""
|
|
69
76
|
has_previous: bool = False
|
|
77
|
+
"""Whether records precede the page."""
|
|
70
78
|
|
|
71
79
|
|
|
72
80
|
def build_list_query(fltr: Any, sort: str | None, offset: int = 0, limit: int | None = None) -> ListQuery:
|
|
@@ -6,7 +6,7 @@ from typing import Annotated, Any, final, Generic, get_args, get_origin, Union
|
|
|
6
6
|
|
|
7
7
|
from fastapi import APIRouter, Header, Query
|
|
8
8
|
from fastapi.exceptions import RequestValidationError
|
|
9
|
-
from pydantic import BaseModel, ConfigDict, create_model
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, create_model, Field
|
|
10
10
|
from pydantic.alias_generators import to_camel
|
|
11
11
|
from starlette.exceptions import HTTPException
|
|
12
12
|
from typing_extensions import TypeVar
|
|
@@ -200,13 +200,22 @@ class SortStateColumn(BaseModel):
|
|
|
200
200
|
|
|
201
201
|
model_config = ConfigDict(populate_by_name=True, alias_generator=to_camel)
|
|
202
202
|
|
|
203
|
-
column_name: str
|
|
204
|
-
direction: SortDirection = SortDirection.asc
|
|
203
|
+
column_name: str = Field(description="Name of the field to order by.")
|
|
204
|
+
direction: SortDirection = Field(SortDirection.asc, description="`asc` or `desc`.")
|
|
205
205
|
|
|
206
206
|
|
|
207
207
|
SortState = list[SortStateColumn]
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
OFFSET_DESCRIPTION = "Number of records to skip before the page."
|
|
211
|
+
"""The `offset` parameter's description on every paginated list endpoint."""
|
|
212
|
+
|
|
213
|
+
CURSOR_DESCRIPTION = (
|
|
214
|
+
"Position to read from: `next`, `previous`, `first` or `last` of a page returned by the same ordering and"
|
|
215
|
+
" filter. Omit for the first page."
|
|
216
|
+
)
|
|
217
|
+
"""The `cursor` parameter's description on every cursor list endpoint."""
|
|
218
|
+
|
|
210
219
|
SORT_DESCRIPTION = (
|
|
211
220
|
"Ordering: comma-separated `column:direction` entries in priority order (`name:asc,score:desc`)."
|
|
212
221
|
" The direction is `asc` or `desc`, `asc` when omitted; an entry with any other direction is"
|
|
@@ -318,9 +327,9 @@ class ListMixin(Generic[T, TFilter], ABC):
|
|
|
318
327
|
context: Context,
|
|
319
328
|
fltr: Annotated[TFilter, Query()] = None,
|
|
320
329
|
sort: Annotated[str | None, Query(description=SORT_DESCRIPTION)] = None,
|
|
321
|
-
offset: Annotated[int, Query(ge=0)] = 0,
|
|
322
|
-
limit: Annotated[int | None, Query(ge=1)] = None,
|
|
323
|
-
cursor: str | None = None,
|
|
330
|
+
offset: Annotated[int, Query(ge=0, description=OFFSET_DESCRIPTION)] = 0,
|
|
331
|
+
limit: Annotated[int | None, Query(ge=1, description="Page size.")] = None,
|
|
332
|
+
cursor: Annotated[str | None, Query(description=CURSOR_DESCRIPTION)] = None,
|
|
324
333
|
x_list_shape: Annotated[str | None, Header()] = None,
|
|
325
334
|
) -> Union[list[T], PaginatedList[T], CursorPage[T]]: # noqa: UP007 - see list_shapes.response_model
|
|
326
335
|
"""List the collection."""
|
|
@@ -705,16 +714,18 @@ class BulkDestroyMixin(DestroyMixin[K, T], BulkOnlyDestroyMixin[K, T]):
|
|
|
705
714
|
# LOOKUP
|
|
706
715
|
###################################################################################################
|
|
707
716
|
class LookupItem(BaseModel):
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
717
|
+
"""One choice of a lookup."""
|
|
718
|
+
|
|
719
|
+
group: Any = Field(None, description="Group the choice is listed under; null for an ungrouped choice.")
|
|
720
|
+
pk: object = Field(description="Primary key of the record.")
|
|
721
|
+
title: str = Field(description="Text shown for the choice.")
|
|
722
|
+
icon: str | None = Field(None, description="Icon shown with the choice; null for none.")
|
|
712
723
|
|
|
713
724
|
|
|
714
725
|
class LookupFilter(BaseModel):
|
|
715
726
|
"""Default filter model for LookupMixin. Provides case-insensitive title search via q."""
|
|
716
727
|
|
|
717
|
-
q: str | None = None
|
|
728
|
+
q: str | None = Field(None, description="Text the choice's title contains, case-insensitive. Omit for all choices.")
|
|
718
729
|
|
|
719
730
|
|
|
720
731
|
TLookupFilter = TypeVar("TLookupFilter", default=LookupFilter)
|
|
@@ -9,6 +9,7 @@ from typing import Any
|
|
|
9
9
|
|
|
10
10
|
from fastapi import FastAPI
|
|
11
11
|
from fastapi.routing import APIRoute
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
12
13
|
from starlette.exceptions import HTTPException
|
|
13
14
|
from starlette.routing import Match
|
|
14
15
|
from starlette.types import Message, Receive, Scope, Send
|
|
@@ -78,6 +79,30 @@ def transport_route_class(base: type[APIRoute], transports: frozenset[str]) -> t
|
|
|
78
79
|
return type(name, (TransportRoute, base), {"transports": transports})
|
|
79
80
|
|
|
80
81
|
|
|
82
|
+
class OpenAPIDocument(BaseModel):
|
|
83
|
+
"""An OpenAPI 3.1 document (https://spec.openapis.org/oas/v3.1.0); members not listed here are kept."""
|
|
84
|
+
|
|
85
|
+
model_config = ConfigDict(extra="allow")
|
|
86
|
+
|
|
87
|
+
openapi: str = Field(description="Version of the OpenAPI specification the document follows.")
|
|
88
|
+
info: dict[str, Any] = Field(description="Title and version of the API.")
|
|
89
|
+
paths: dict[str, Any] = Field(description="The endpoints, by path and HTTP method.")
|
|
90
|
+
components: dict[str, Any] | None = Field(None, description="Schemas the endpoints refer to.")
|
|
91
|
+
tags: list[dict[str, Any]] | None = Field(None, description="Names and descriptions of the endpoint groups.")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
SCHEMA_ENDPOINT_DOCS: dict[str, Any] = {
|
|
95
|
+
"description": (
|
|
96
|
+
"OpenAPI document of this viewset's endpoints on the transport it is requested over (REST or muxws),"
|
|
97
|
+
" this endpoint included."
|
|
98
|
+
),
|
|
99
|
+
"response_description": "The OpenAPI document.",
|
|
100
|
+
"responses": {200: {"model": OpenAPIDocument}},
|
|
101
|
+
"response_model": None,
|
|
102
|
+
}
|
|
103
|
+
"""`add_api_route` keyword arguments of every viewset's `/schema` endpoint; `@endpoint_docs` key `schema`."""
|
|
104
|
+
|
|
105
|
+
|
|
81
106
|
def openapi_endpoint(route_kwargs: list[dict[str, Any]]) -> Callable[[], dict[str, Any]]:
|
|
82
107
|
"""
|
|
83
108
|
A `/schema` endpoint with the OpenAPI document of `route_kwargs` (`add_api_route` form), built on
|
{dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/pyproject.toml
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dynamicforms-fastapi-viewsets"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.1"
|
|
8
8
|
description = "Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
{dynamicforms_fastapi_viewsets-0.15.0 → dynamicforms_fastapi_viewsets-0.15.1}/pyproject.toml.orig
RENAMED
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "dynamicforms-fastapi-viewsets"
|
|
7
|
-
version = "0.15.
|
|
7
|
+
version = "0.15.1"
|
|
8
8
|
description = "Django REST Framework-style viewsets for FastAPI, with optional Celery-backed async execution and a Vue/TypeScript client counterpart."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|