dynamicforms-fastapi-viewsets 0.7.0__tar.gz → 0.7.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.
Files changed (53) hide show
  1. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/PKG-INFO +1 -1
  2. dynamicforms_fastapi_viewsets-0.7.1/fastapi_viewsets/__init__.py +1 -0
  3. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/context/__init__.py +40 -0
  4. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/build_schema.py +6 -0
  5. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/celery_viewset/client.py +6 -7
  6. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +5 -1
  7. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/celery_viewset/server.py +7 -8
  8. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/lifecycle_runner.py +5 -1
  9. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/route_viewset.py +3 -2
  10. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/middleware/__init__.py +52 -4
  11. dynamicforms_fastapi_viewsets-0.7.0/fastapi_viewsets/__init__.py +0 -1
  12. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/.gitignore +0 -0
  13. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/LICENSE +0 -0
  14. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/README.md +0 -0
  15. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/action_configuration.py +0 -0
  16. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/backends/__init__.py +0 -0
  17. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/backends/django_orm.py +0 -0
  18. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/collection_viewset.py +0 -0
  19. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/conf.py +0 -0
  20. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/context/auth/__init__.py +0 -0
  21. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/context/auth/django.py +0 -0
  22. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/context/auth/jwt.py +0 -0
  23. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/context/auth/static.py +0 -0
  24. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/cursor.py +0 -0
  25. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/__init__.py +0 -0
  26. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
  27. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/conftest.py +0 -0
  28. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
  29. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/endpoint_docs.py +0 -0
  30. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/exceptions.py +0 -0
  31. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/filters/__init__.py +0 -0
  32. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/filters/base.py +0 -0
  33. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/filters/declaration.py +0 -0
  34. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/filters/operators.py +0 -0
  35. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/filters/registry.py +0 -0
  36. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/list_query.py +0 -0
  37. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/list_shapes.py +0 -0
  38. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
  39. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
  40. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
  41. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mixins.py +0 -0
  42. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/__init__.py +0 -0
  43. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/asgi.py +0 -0
  44. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/protocol.py +0 -0
  45. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/registry.py +0 -0
  46. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/server.py +0 -0
  47. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/mux_ws/transports.py +0 -0
  48. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/py.typed +0 -0
  49. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/response_classes.py +0 -0
  50. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/save_state/__init__.py +0 -0
  51. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/save_state/save_state.py +0 -0
  52. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/fastapi_viewsets/save_state/serialize_state.py +0 -0
  53. {dynamicforms_fastapi_viewsets-0.7.0 → dynamicforms_fastapi_viewsets-0.7.1}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dynamicforms-fastapi-viewsets
3
- Version: 0.7.0
3
+ Version: 0.7.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
  Project-URL: Homepage, https://github.com/dynamicforms/fastapi-viewsets
6
6
  Project-URL: Repository, https://github.com/dynamicforms/fastapi-viewsets
@@ -0,0 +1 @@
1
+ __version__ = "0.7.1"
@@ -6,6 +6,8 @@ import json
6
6
  from abc import ABC, abstractmethod
7
7
  from typing import Any, TYPE_CHECKING
8
8
 
9
+ from pydantic import BaseModel
10
+
9
11
  from ..conf import settings
10
12
 
11
13
  if TYPE_CHECKING:
@@ -300,6 +302,44 @@ def _resolve_class(dotted_path: str) -> type[SerializableObject]:
300
302
  return cls
301
303
 
302
304
 
305
+ def serialize_value(value: Any) -> Any:
306
+ """
307
+ Tag a single SerializableObject for JSON transport, the same convention serialize_context()
308
+ applies per context key (see _TYPE_KEY/_VALUE_KEY above); anything else passes through
309
+ unchanged. For a value crossing the Celery/Redis boundary outside a Context dict - e.g. a
310
+ command-middleware ViewSetResult returned by a celery_viewset-dispatched action (see
311
+ fastapi_viewsets.middleware.ViewSetResult).
312
+ """
313
+ if isinstance(value, SerializableObject):
314
+ return {_TYPE_KEY: _class_tag(type(value)), _VALUE_KEY: value.__serialize__()}
315
+ return value
316
+
317
+
318
+ def to_jsonable(value: Any) -> Any:
319
+ """
320
+ Recursively convert a value into a JSON-safe structure: a SerializableObject is tagged (see
321
+ serialize_value()), a Pydantic model is dumped, a list/tuple is recursed into element-wise -
322
+ anything else passes through unchanged. The one shared implementation of a conversion several
323
+ call sites need on their own single (non-Context, non-dict) return/body value - ViewSetResult's
324
+ own (de)serialization, and a celery_viewset worker's task result - rather than each keeping a
325
+ slightly different copy that drifts out of sync with what the others handle.
326
+ """
327
+ if isinstance(value, SerializableObject):
328
+ return serialize_value(value)
329
+ if isinstance(value, BaseModel):
330
+ return value.model_dump(mode="json")
331
+ if isinstance(value, (list, tuple)):
332
+ return [to_jsonable(v) for v in value]
333
+ return value
334
+
335
+
336
+ def deserialize_value(value: Any) -> Any:
337
+ """Inverse of serialize_value() - passes through anything that isn't a tagged payload."""
338
+ if isinstance(value, dict) and _TYPE_KEY in value and _VALUE_KEY in value:
339
+ return _resolve_class(value[_TYPE_KEY]).__deserialize__(value[_VALUE_KEY])
340
+ return value
341
+
342
+
303
343
  async def serialize_context(data: dict[str, Any]) -> dict[str, Any]:
304
344
  """
305
345
  Returns a JSON-safe dict: SerializableObject values are swapped for a tagged payload (see
@@ -6,6 +6,7 @@ from fastapi import APIRouter, FastAPI
6
6
  from fastapi.routing import APIRoute
7
7
 
8
8
  from ..endpoint_docs import docs_for, viewset_tags
9
+ from ..middleware import unwrap_viewset_result_type
9
10
 
10
11
 
11
12
  def _generic_args(annotation) -> tuple:
@@ -166,6 +167,11 @@ def build_schema(cls, base_path: str = "", default_tags=None, get_wrapper=None,
166
167
  # answer: only route_viewset knows what this viewset bound it to.
167
168
  use_resolved = route.response_model is None or has_typevars(route.response_model)
168
169
  response_model = resolved_response_model if use_resolved else route.response_model
170
+ # route.response_model is FastAPI's own inference from the ORIGINAL declared return
171
+ # type (e.g. -> ViewSetResult[Item]), untouched by get_wrapper's own unwrap - must be
172
+ # unwrapped here too, or an endpoint declared this way would document/validate as the
173
+ # wrapper instead of the wire body whenever use_resolved is False.
174
+ response_model = unwrap_viewset_result_type(response_model)
169
175
 
170
176
  # Per-viewset wording, applied here because this is where the schema-serving app is
171
177
  # built; patching the caller's router afterwards would leave /schema showing the
@@ -6,9 +6,7 @@ from collections.abc import Awaitable, Callable
6
6
  from functools import wraps
7
7
  from typing import TYPE_CHECKING, TypeVar
8
8
 
9
- from pydantic import BaseModel
10
-
11
- from ...context import Context, serialize_context
9
+ from ...context import Context, serialize_context, to_jsonable
12
10
  from ..build_schema import build_schema
13
11
  from . import result_reader
14
12
  from .result_reader import get_result_queue_key
@@ -84,16 +82,17 @@ def celery_viewset_client(
84
82
 
85
83
 
86
84
  async def _serialize_value(value):
87
- """Convert a Context, BaseModel, list, or dict value into a JSON-safe structure for Celery/Kombu transport."""
85
+ """Convert a Context, dict, or list/tuple value into a JSON-safe structure for Celery/Kombu
86
+ transport, recursing async since Context serialization needs to await context values. Anything
87
+ else - a Pydantic model, a SerializableObject, or an already JSON-native value - is handled by
88
+ the shared, synchronous fastapi_viewsets.context.to_jsonable()."""
88
89
  if isinstance(value, Context):
89
90
  return await serialize_context(value.raw())
90
- if isinstance(value, BaseModel):
91
- return value.model_dump(mode="json")
92
91
  if isinstance(value, dict):
93
92
  return {k: await _serialize_value(v) for k, v in value.items()}
94
93
  if isinstance(value, (list, tuple)):
95
94
  return [await _serialize_value(v) for v in value]
96
- return value
95
+ return to_jsonable(value)
97
96
 
98
97
 
99
98
  def _patch_method(cls: type, original_endpoint, task_name: str, celery_app, queue_key: str):
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
6
6
 
7
7
  from fastapi import HTTPException
8
8
 
9
+ from ...context import deserialize_value
10
+
9
11
  if TYPE_CHECKING:
10
12
  import redis
11
13
 
@@ -59,7 +61,9 @@ async def result_reader_loop(redis_client: "redis.Redis", queue_key: str, poll_i
59
61
  else:
60
62
  future.set_exception(Exception(error))
61
63
  else:
62
- future.set_result(result)
64
+ # Undoes _to_jsonable's SerializableObject tagging (see server.py) - a plain
65
+ # domain result has no _TYPE_KEY/_VALUE_KEY pair and passes through unchanged.
66
+ future.set_result(deserialize_value(result))
63
67
 
64
68
  except asyncio.CancelledError:
65
69
  break
@@ -11,7 +11,7 @@ from typing import get_args, get_origin, get_type_hints, TYPE_CHECKING, TypeVar,
11
11
  from fastapi import HTTPException
12
12
  from pydantic import BaseModel
13
13
 
14
- from ...context import Context, deserialize_context
14
+ from ...context import Context, deserialize_context, to_jsonable
15
15
  from ..build_schema import build_schema
16
16
  from ..lifecycle_runner import lifecycle_runner, LifecycleType
17
17
  from ..route_viewset import build_type_map, resolve_typevars
@@ -50,13 +50,12 @@ def _unwrap_optional(hint):
50
50
  return hint
51
51
 
52
52
 
53
- def _to_jsonable(value):
54
- """Recursively convert Pydantic models and lists to JSON-serializable structures."""
55
- if isinstance(value, BaseModel):
56
- return value.model_dump(mode="json")
57
- if isinstance(value, list):
58
- return [_to_jsonable(v) for v in value]
59
- return value
53
+ # Alias, not a re-implementation: the actual conversion (Pydantic models, lists/tuples,
54
+ # SerializableObject values - e.g. a ViewSetResult a worker-side action returned directly, see
55
+ # fastapi_viewsets.middleware) lives in fastapi_viewsets.context.to_jsonable, shared with
56
+ # ViewSetResult's own (de)serialization. Kept under this name since call sites/tests here already
57
+ # import it as `_to_jsonable`.
58
+ _to_jsonable = to_jsonable
60
59
 
61
60
 
62
61
  def _reconstruct_value(hint, value):
@@ -68,7 +68,11 @@ async def lifecycle_runner(
68
68
  return await call_bound(req_instance)
69
69
 
70
70
  async def final_handler() -> ViewSetResult:
71
- return ViewSetResult(body=await call_bound(req_instance))
71
+ raw = await call_bound(req_instance)
72
+ # An endpoint may return a ViewSetResult directly (see ViewSetResult in
73
+ # fastapi_viewsets/middleware) to reach status_code/headers/cookies itself - passed
74
+ # through as-is.
75
+ return raw if isinstance(raw, ViewSetResult) else ViewSetResult(body=raw)
72
76
 
73
77
  effective_middlewares = settings.viewsets_command_middleware + extra_middlewares_for(
74
78
  action_configuration, settings.viewsets_command_middleware
@@ -21,7 +21,7 @@ from fastapi_viewsets.endpoint_docs import (
21
21
  register_tag,
22
22
  viewset_description,
23
23
  )
24
- from fastapi_viewsets.middleware import any_modifies_response_shape, Middleware
24
+ from fastapi_viewsets.middleware import any_modifies_response_shape, Middleware, unwrap_viewset_result_type
25
25
  from fastapi_viewsets.mixins import FilterParam
26
26
  from fastapi_viewsets.mux_ws import register_viewset, resolve_register_muxws, resolve_register_rest
27
27
 
@@ -298,7 +298,8 @@ def route_viewset(
298
298
  # right before the endpoint runs - never forwarded to the endpoint itself.
299
299
  new_params.append(inspect.Parameter("request", inspect.Parameter.KEYWORD_ONLY, annotation=Request))
300
300
 
301
- new_return_annotation = resolve_typevars(type_map, sig.return_annotation)
301
+ # -> ViewSetResult[X] documents/validates as X - see unwrap_viewset_result_type.
302
+ new_return_annotation = unwrap_viewset_result_type(resolve_typevars(type_map, sig.return_annotation))
302
303
  new_sig = sig.replace(parameters=new_params, return_annotation=new_return_annotation)
303
304
 
304
305
  @wraps(original_endpoint)
@@ -1,16 +1,18 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Awaitable, Callable
3
3
  from dataclasses import dataclass, field
4
- from typing import Any, TYPE_CHECKING
4
+ from typing import Any, Generic, get_args, get_origin, TYPE_CHECKING, TypeVar
5
5
 
6
- from ..context import Context
6
+ from ..context import Context, SerializableObject, to_jsonable
7
7
 
8
8
  if TYPE_CHECKING:
9
9
  from fastapi import Request
10
10
 
11
+ T = TypeVar("T")
12
+
11
13
 
12
14
  @dataclass
13
- class ViewSetResult:
15
+ class ViewSetResult(SerializableObject, Generic[T]):
14
16
  """
15
17
  Transport-agnostic result of running the command middleware chain (see run_command_chain()
16
18
  below). `body` is the actual domain value (whatever perform_* returned); `headers`/`cookies`
@@ -22,13 +24,44 @@ class ViewSetResult:
22
24
  onto real Set-Cookie, and `status_code` onto the real Response's status code; a future WS
23
25
  adapter would fold all three into the outgoing message payload as plain JSON keys instead (no
24
26
  real headers/cookies/status line exist over WS).
27
+
28
+ A registered route endpoint (a mixin-provided one, or a custom `__router` method - never a
29
+ `perform_*` hook, which is called by the actual route method and never registered as a route
30
+ itself) may also return a `ViewSetResult` directly instead of a plain body, getting the same
31
+ `status_code`/`headers`/`cookies` access normally reserved for command middleware (e.g. a
32
+ redirect: `ViewSetResult(body=None, status_code=302, headers={"Location": url})`) -
33
+ `lifecycle_runner.final_handler` passes it through unchanged. Declare the return type as
34
+ `ViewSetResult[X]` (X being whatever the wire body actually is) rather than bare `X` -
35
+ `route_viewset`/`build_schema` unwrap this to `X` for the OpenAPI response_model and FastAPI's
36
+ own response validation (see unwrap_viewset_result_type below), so the two forms
37
+ document/validate identically.
38
+
39
+ Inherits `SerializableObject` so it survives the Celery/Redis boundary: a
40
+ `celery_viewset`-dispatched action's worker-side return value is passed through
41
+ `serialize_value()`/`deserialize_value()` (see fastapi_viewsets.context) exactly like any other
42
+ `SerializableObject`, the same mechanism `LazyObject`/`Context` values already rely on. Only
43
+ `__serialize__`/`__deserialize__` are overridden here - a `ViewSetResult` is never itself placed
44
+ in a `Context` and never awaited, so `SerializableObject`'s `value`/`__await__`/`__init__` go
45
+ unused, the same way `LazyObject` replaces those with its own.
25
46
  """
26
47
 
27
- body: Any
48
+ body: T
28
49
  headers: dict[str, Any] = field(default_factory=dict)
29
50
  cookies: dict[str, Any] = field(default_factory=dict)
30
51
  status_code: int | None = None
31
52
 
53
+ def __serialize__(self) -> Any:
54
+ return {
55
+ "body": to_jsonable(self.body),
56
+ "headers": self.headers,
57
+ "cookies": self.cookies,
58
+ "status_code": self.status_code,
59
+ }
60
+
61
+ @classmethod
62
+ def __deserialize__(cls, data: Any) -> "ViewSetResult":
63
+ return cls(body=data["body"], headers=data["headers"], cookies=data["cookies"], status_code=data["status_code"])
64
+
32
65
 
33
66
  CommandMiddleware = Callable[
34
67
  ["Request | None", Any, Context, Callable[[], Awaitable[ViewSetResult]]],
@@ -130,6 +163,21 @@ def any_modifies_response_shape(middlewares: list) -> bool:
130
163
  return any(getattr(middleware, "modifies_response_shape", False) for middleware in middlewares)
131
164
 
132
165
 
166
+ def unwrap_viewset_result_type(annotation: Any) -> Any:
167
+ """
168
+ An endpoint declared `-> ViewSetResult[X]` (see ViewSetResult above) documents/validates as
169
+ `X` - the wire body a client actually receives, not the side-channel wrapper the endpoint
170
+ returns internally to reach `status_code`/`headers`/`cookies`. `route_viewset`/`build_schema`
171
+ call this wherever a return annotation becomes an OpenAPI response_model, so `-> ViewSetResult[X]`
172
+ and plain `-> X` document/validate identically. Anything that isn't `ViewSetResult[...]` passes
173
+ through unchanged.
174
+ """
175
+ if get_origin(annotation) is ViewSetResult:
176
+ args = get_args(annotation)
177
+ return args[0] if args else None
178
+ return annotation
179
+
180
+
133
181
  async def run_command_chain(
134
182
  middlewares: list[CommandMiddleware],
135
183
  request: "Request | None",
@@ -1 +0,0 @@
1
- __version__ = "0.7.0"