dynamicforms-fastapi-viewsets 0.1.0__tar.gz → 0.1.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 (36) hide show
  1. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/PKG-INFO +13 -4
  2. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/README.md +4 -3
  3. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/__init__.py +1 -0
  4. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/action_configuration.py +111 -0
  5. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/collection_viewset.py +11 -8
  6. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/conf.py +45 -0
  7. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/context/__init__.py +395 -0
  8. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/context/auth/__init__.py +55 -0
  9. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/context/auth/django.py +126 -0
  10. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/context/auth/jwt.py +86 -0
  11. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/context/auth/static.py +87 -0
  12. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/build_schema.py +6 -5
  13. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/celery_viewset/client.py +9 -4
  14. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/celery_viewset/server.py +18 -4
  15. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/decorators/lifecycle_runner.py +108 -0
  16. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/route_viewset.py +75 -15
  17. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/middleware/__init__.py +122 -0
  18. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/middleware/auth/__init__.py +53 -0
  19. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/middleware/auth/authorization.py +67 -0
  20. dynamicforms_fastapi_viewsets-0.1.1/fastapi_viewsets/middleware/rate_limiter.py +101 -0
  21. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/mixins.py +42 -29
  22. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/pyproject.toml +5 -0
  23. dynamicforms_fastapi_viewsets-0.1.0/fastapi_viewsets/__init__.py +0 -1
  24. dynamicforms_fastapi_viewsets-0.1.0/fastapi_viewsets/celery_viewset.py +0 -50
  25. dynamicforms_fastapi_viewsets-0.1.0/fastapi_viewsets/decorators/lifecycle_runner.py +0 -64
  26. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/.gitignore +0 -0
  27. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/LICENSE +0 -0
  28. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/__init__.py +0 -0
  29. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -0
  30. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +0 -0
  31. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
  32. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/py.typed +0 -0
  33. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/response_classes.py +0 -0
  34. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/save_state/__init__.py +0 -0
  35. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.1}/fastapi_viewsets/save_state/save_state.py +0 -0
  36. {dynamicforms_fastapi_viewsets-0.1.0 → dynamicforms_fastapi_viewsets-0.1.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.1.0
3
+ Version: 0.1.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
@@ -29,13 +29,21 @@ Provides-Extra: celery
29
29
  Requires-Dist: celery>=5.3; extra == 'celery'
30
30
  Requires-Dist: redis>=5.0; extra == 'celery'
31
31
  Provides-Extra: dev
32
+ Requires-Dist: asgiref; extra == 'dev'
32
33
  Requires-Dist: celery; extra == 'dev'
33
34
  Requires-Dist: coverage; extra == 'dev'
35
+ Requires-Dist: django; extra == 'dev'
34
36
  Requires-Dist: httpx; extra == 'dev'
37
+ Requires-Dist: pyjwt; extra == 'dev'
35
38
  Requires-Dist: pytest; extra == 'dev'
36
39
  Requires-Dist: pytest-asyncio; extra == 'dev'
37
40
  Requires-Dist: pytest-cov; extra == 'dev'
38
41
  Requires-Dist: redis; extra == 'dev'
42
+ Provides-Extra: django
43
+ Requires-Dist: asgiref>=3.6; extra == 'django'
44
+ Requires-Dist: django>=4.2; extra == 'django'
45
+ Provides-Extra: jwt
46
+ Requires-Dist: pyjwt>=2.8; extra == 'jwt'
39
47
  Description-Content-Type: text/markdown
40
48
 
41
49
  # DynamicForms FastAPI Viewsets
@@ -48,8 +56,9 @@ Celery-backed async execution and a matching Vue/TypeScript client counterpart.
48
56
  Handles type resolution, lifecycle management and OpenAPI schema automatically.
49
57
  - **`CollectionViewSet`** — zero-boilerplate in-memory viewset backed by any Python list, set or dict.
50
58
  Great for prototyping and testing.
51
- - **`CeleryViewSet`**delegate all CRUD operations to Celery tasks, for long-running or background
52
- processing scenarios (requires the `celery` extra).
59
+ - **`celery_viewset` decorator** move a viewset's execution to a Celery worker with no code changes
60
+ to the viewset itself, for long-running or background processing scenarios (requires the `celery`
61
+ extra).
53
62
  - **Bulk operations** — first-class support for bulk create, update, partial update and destroy.
54
63
  - **Vue / TypeScript counterpart** — mirror mixin classes and a `route_rest` factory give you a fully
55
64
  typed HTTP client that matches your backend viewset exactly (published separately as
@@ -98,7 +107,7 @@ app.include_router(router)
98
107
  ```
99
108
 
100
109
  See the [full documentation](https://docs.velis.si/dynamicforms/fastapi-viewsets/) for guides on
101
- the mixin system, `route_viewset`, `CollectionViewSet`, `CeleryViewSet`, and the Vue client.
110
+ the mixin system, `route_viewset`, `CollectionViewSet`, `celery_viewset`, and the Vue client.
102
111
 
103
112
  ## License
104
113
 
@@ -8,8 +8,9 @@ Celery-backed async execution and a matching Vue/TypeScript client counterpart.
8
8
  Handles type resolution, lifecycle management and OpenAPI schema automatically.
9
9
  - **`CollectionViewSet`** — zero-boilerplate in-memory viewset backed by any Python list, set or dict.
10
10
  Great for prototyping and testing.
11
- - **`CeleryViewSet`**delegate all CRUD operations to Celery tasks, for long-running or background
12
- processing scenarios (requires the `celery` extra).
11
+ - **`celery_viewset` decorator** move a viewset's execution to a Celery worker with no code changes
12
+ to the viewset itself, for long-running or background processing scenarios (requires the `celery`
13
+ extra).
13
14
  - **Bulk operations** — first-class support for bulk create, update, partial update and destroy.
14
15
  - **Vue / TypeScript counterpart** — mirror mixin classes and a `route_rest` factory give you a fully
15
16
  typed HTTP client that matches your backend viewset exactly (published separately as
@@ -58,7 +59,7 @@ app.include_router(router)
58
59
  ```
59
60
 
60
61
  See the [full documentation](https://docs.velis.si/dynamicforms/fastapi-viewsets/) for guides on
61
- the mixin system, `route_viewset`, `CollectionViewSet`, `CeleryViewSet`, and the Vue client.
62
+ the mixin system, `route_viewset`, `CollectionViewSet`, `celery_viewset`, and the Vue client.
62
63
 
63
64
  ## License
64
65
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,111 @@
1
+ from typing import Any
2
+
3
+ from .conf import settings
4
+ from .middleware import Middleware
5
+
6
+ _PERFORM_METHOD_BY_ACTION = {
7
+ "create": "perform_create",
8
+ "bulk_create": "perform_bulk_create",
9
+ "list_items": "perform_list",
10
+ "retrieve": "perform_retrieve",
11
+ "update": "perform_update",
12
+ "partial_update": "perform_update",
13
+ "bulk_update": "perform_bulk_update",
14
+ "bulk_partial_update": "perform_bulk_update",
15
+ "destroy": "perform_destroy",
16
+ "bulk_destroy": "perform_bulk_destroy",
17
+ "lookup": "perform_lookup",
18
+ }
19
+ """Mirrors the action-method -> perform_* wiring already in mixins.py, so @action_configuration on
20
+ a perform_* method (where CRUD logic actually lives) is found when resolving its action's config,
21
+ not just on the framework's own @final action methods (update, list_items, ...)."""
22
+
23
+
24
+ def action_configuration(config: dict[Any, Any]):
25
+ """
26
+ Attaches per-viewset/per-action configuration for context processors and command middleware to
27
+ consult - works identically whether applied to a viewset class or to an individual method
28
+ (typically a perform_* method). Keys are identifiers (a context processor callable, a
29
+ Middleware subclass already registered in settings.viewsets_command_middleware, or a fresh
30
+ Middleware instance to inject just for this call - see extra_middlewares_for); values are
31
+ opaque, interpreted only by whichever processor/middleware reads them (see
32
+ fastapi_viewsets/context/__init__.py's Context.configuration_for and build_context). See
33
+ docs/guide/action-configuration.md.
34
+
35
+ @action_configuration({Session: False})
36
+ class LoginViewSet(...): ...
37
+
38
+ class ItemViewSet(...):
39
+ @action_configuration({Session: False})
40
+ async def perform_list(self, context): ...
41
+ """
42
+
43
+ def decorator(target):
44
+ target.__action_configuration__ = config
45
+ return target
46
+
47
+ return decorator
48
+
49
+
50
+ def _candidate_method_names(action_name: str) -> list[str]:
51
+ names = [action_name]
52
+ perform_name = _PERFORM_METHOD_BY_ACTION.get(action_name)
53
+ if perform_name:
54
+ names.append(perform_name)
55
+ return names
56
+
57
+
58
+ def resolve_action_configuration(cls: type, action_name: str) -> dict[Any, Any]:
59
+ """
60
+ Merges settings.default_action_configuration (lowest priority) -> class-level
61
+ @action_configuration -> method-level @action_configuration (highest priority), per identifier
62
+ (a method-level entry for an identifier fully replaces whatever a lower layer had for that same
63
+ identifier; identifiers a method doesn't mention still fall through from the class/default).
64
+
65
+ Returns the raw merged dict - ByAction values are left unresolved (see
66
+ fastapi_viewsets/context/__init__.py's Context.configuration_for, which resolves them at read
67
+ time against the current action, so a Context reused across actions - e.g. via
68
+ clone_for_command - never resolves stale).
69
+
70
+ Takes the viewset *class* (not an instance) - method-level lookup via `getattr(cls, candidate)`
71
+ returns the same underlying function (and thus the same `__action_configuration__` attribute)
72
+ as `getattr(instance, candidate)` would, so no instance is ever needed; this also lets it be
73
+ called before a viewset instance exists yet (e.g. from an early Middleware.depends() call - see
74
+ route_viewset.py - for per-request/instance-key lifecycles).
75
+
76
+ Method-level lookup checks both the actual action method name (for hand-written __router
77
+ endpoints, which have no separate perform_* counterpart) and its mapped perform_* method (where
78
+ CRUD logic actually lives - see _PERFORM_METHOD_BY_ACTION), in that order, so a perform_*-level
79
+ override wins if both happen to be decorated.
80
+ """
81
+ merged: dict[Any, Any] = dict(settings.default_action_configuration)
82
+ merged.update(getattr(cls, "__action_configuration__", {}))
83
+ for candidate in _candidate_method_names(action_name):
84
+ method_config = getattr(getattr(cls, candidate, None), "__action_configuration__", None)
85
+ if method_config:
86
+ merged.update(method_config)
87
+ return merged
88
+
89
+
90
+ def extra_middlewares_for(action_configuration: dict[Any, Any], global_middlewares: list) -> list:
91
+ """
92
+ Keys in `action_configuration` that are a Middleware *instance* are appended to the chain,
93
+ just for this call, in dict-iteration order - this is how @action_configuration injects a
94
+ brand-new middleware for one viewset/method without registering it globally. Keys that are a
95
+ Middleware *class* configure an already-globally-registered instance of that type (read via
96
+ Middleware.config_from/Context.configuration_for) - a class with no matching instance in
97
+ `global_middlewares` raises ValueError, fail-fast, since it's almost certainly a typo or a
98
+ forgotten global registration rather than an intentional injection (use an instance for that).
99
+ """
100
+ global_types = {type(mw) for mw in global_middlewares}
101
+ extras = []
102
+ for identifier in action_configuration:
103
+ if isinstance(identifier, Middleware):
104
+ extras.append(identifier)
105
+ elif isinstance(identifier, type) and issubclass(identifier, Middleware) and identifier not in global_types:
106
+ raise ValueError(
107
+ f"{identifier.__name__} is configured via @action_configuration but has no instance "
108
+ f"in settings.viewsets_command_middleware - register one globally, or pass a "
109
+ f"Middleware instance (not the class) to inject a new one just for this call."
110
+ )
111
+ return extras
@@ -4,6 +4,7 @@ from typing import Any, Generic, TypeVar
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
+ from .context import Context
7
8
  from .mixins import ImplMixin
8
9
  from .response_classes import NotFoundError
9
10
 
@@ -39,35 +40,37 @@ class AsyncCollectionViewSet(
39
40
  async def container_delete(self, pk: K) -> None:
40
41
  raise NotImplementedError
41
42
 
42
- async def perform_list(self) -> list[T]:
43
+ async def perform_list(self, _context: Context) -> list[T]:
43
44
  return await self.container_list()
44
45
 
45
- async def perform_retrieve(self, pk: K) -> T:
46
+ async def perform_retrieve(self, _context: Context, pk: K) -> T:
46
47
  return await self.container_retrieve(pk)
47
48
 
48
- async def perform_create(self, data: T) -> T:
49
+ async def perform_create(self, _context: Context, data: T) -> T:
49
50
  return await self.container_add(data)
50
51
 
51
- async def perform_bulk_create(self, data: list[T]) -> list[T]:
52
+ async def perform_bulk_create(self, _context: Context, data: list[T]) -> list[T]:
52
53
  results = []
53
54
  for item in data:
54
55
  results.append(await self.container_add(item))
55
56
  return results
56
57
 
57
- async def perform_update(self, pk: K, data: T, partial: bool = True) -> T:
58
+ async def perform_update(self, _context: Context, pk: K, data: T, partial: bool = True) -> T:
58
59
  return await self.container_update(pk, data, partial)
59
60
 
60
- async def perform_bulk_update(self, records: dict[K, T], partial: bool = True) -> list[T]:
61
+ async def perform_bulk_update(
62
+ self, _context: Context, records: dict[K, T], partial: bool = True
63
+ ) -> list[T]:
61
64
  results = []
62
65
  for pk, data in records.items():
63
66
  results.append(await self.container_update(pk, data, partial))
64
67
  return results
65
68
 
66
- async def perform_destroy(self, pk: K) -> dict[K, Any]:
69
+ async def perform_destroy(self, _context: Context, pk: K) -> dict[K, Any]:
67
70
  await self.container_delete(pk)
68
71
  return {pk: None}
69
72
 
70
- async def perform_bulk_destroy(self, pk: list[K]) -> list[dict[K, Any]]:
73
+ async def perform_bulk_destroy(self, _context: Context, pk: list[K]) -> list[dict[K, Any]]:
71
74
  results = []
72
75
  for p in pk:
73
76
  await self.container_delete(p)
@@ -0,0 +1,45 @@
1
+ import json
2
+
3
+ from collections.abc import Awaitable, Callable
4
+ from typing import Any, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from fastapi import Request
8
+ from fastapi.security.base import SecurityBase
9
+
10
+ from .context.auth import AuthBackend
11
+ from .middleware import CommandMiddleware
12
+
13
+ ContextProcessor = Callable[["Request", Any], Awaitable[dict[str, Any]]]
14
+
15
+
16
+ class Settings:
17
+ """
18
+ Global, mutable settings for fastapi_viewsets - configured once by the application, e.g.:
19
+
20
+ from fastapi_viewsets.conf import settings
21
+
22
+ async def auth_processor(request, viewset) -> dict:
23
+ return {"user": await authenticate(request)}
24
+
25
+ settings.viewsets_context_processors = [auth_processor]
26
+ """
27
+
28
+ def __init__(self):
29
+ self.viewsets_context_processors: list[ContextProcessor] = []
30
+ self.viewsets_context_json_encoder: type[json.JSONEncoder] | None = None
31
+ self.viewsets_context_json_decoder: type[json.JSONDecoder] | None = None
32
+ self.viewsets_command_middleware: list[CommandMiddleware] = []
33
+ self.viewsets_auth_processors: list[AuthBackend] = []
34
+ self.default_action_configuration: dict[Any, Any] = {}
35
+ self.viewsets_security_scheme: SecurityBase | None = None
36
+ """
37
+ Purely for OpenAPI/Swagger discoverability (a lock icon + testable Authorize flow) - attached
38
+ as an extra sub-dependency alongside the per-route Middleware.depends() bridge (see
39
+ route_viewset.py), not otherwise consumed by this library. Must be set before any viewset
40
+ class is decorated with route_viewset (same existing constraint as
41
+ disable_response_model=bool(settings.viewsets_command_middleware) already has).
42
+ """
43
+
44
+
45
+ settings = Settings()