dynamicforms-fastapi-viewsets 0.3.4__tar.gz → 0.3.5__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.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/PKG-INFO +1 -1
- dynamicforms_fastapi_viewsets-0.3.5/fastapi_viewsets/__init__.py +1 -0
- dynamicforms_fastapi_viewsets-0.3.5/fastapi_viewsets/decorators/celery_viewset/__init__.py +82 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/celery_viewset/client.py +10 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/celery_viewset/result_reader.py +37 -20
- dynamicforms_fastapi_viewsets-0.3.4/fastapi_viewsets/__init__.py +0 -1
- dynamicforms_fastapi_viewsets-0.3.4/fastapi_viewsets/decorators/celery_viewset/__init__.py +0 -51
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/.gitignore +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/LICENSE +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/README.md +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/action_configuration.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/collection_viewset.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/conf.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/context/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/context/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/context/auth/django.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/context/auth/jwt.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/context/auth/static.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/build_schema.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/celery_viewset/server.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/lifecycle_runner.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/primary_key_model_helper.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/decorators/route_viewset.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/middleware/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/middleware/auth/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/middleware/auth/authorization.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/middleware/rate_limiter.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/mixins.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/py.typed +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/response_classes.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/save_state/__init__.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/save_state/save_state.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/save_state/serialize_state.py +0 -0
- {dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dynamicforms-fastapi-viewsets
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
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.3.5"
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from ..lifecycle_runner import LifecycleType
|
|
5
|
+
from .client import celery_viewset_client, get_registered_queue_keys
|
|
6
|
+
from .result_reader import get_result_queue_key, get_running_queue_keys, push_result, start_result_reader, stop_result_reader
|
|
7
|
+
from .server import celery_viewset_server
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
# Allow explicit override via set_is_celery_worker()
|
|
12
|
+
_is_celery_worker: bool | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def set_is_celery_worker(is_worker: bool) -> None:
|
|
16
|
+
"""Explicitly set whether we are running in a Celery worker context."""
|
|
17
|
+
global _is_celery_worker
|
|
18
|
+
_is_celery_worker = is_worker
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _detect_is_celery_worker() -> bool:
|
|
22
|
+
"""Auto-detect if we are running inside a Celery worker by inspecting sys.argv."""
|
|
23
|
+
if _is_celery_worker is not None:
|
|
24
|
+
return _is_celery_worker
|
|
25
|
+
return len(sys.argv) > 0 and "celery" in sys.argv[0].lower()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def celery_viewset(celery_app, task_prefix: str, lifecycle: LifecycleType = "singleton", redis_client=None):
|
|
29
|
+
"""
|
|
30
|
+
Convenience decorator that auto-detects whether to use client or server mode.
|
|
31
|
+
|
|
32
|
+
- In a Celery worker (detected via sys.argv or set_is_celery_worker): uses celery_viewset_server
|
|
33
|
+
- In a FastAPI app: uses celery_viewset_client
|
|
34
|
+
|
|
35
|
+
For explicit control, use celery_viewset_client or celery_viewset_server directly.
|
|
36
|
+
"""
|
|
37
|
+
if _detect_is_celery_worker():
|
|
38
|
+
return celery_viewset_server(celery_app, task_prefix, lifecycle=lifecycle, redis_client=redis_client)
|
|
39
|
+
else:
|
|
40
|
+
if redis_client is None:
|
|
41
|
+
raise ValueError("redis_client is required for celery_viewset in client (FastAPI) mode")
|
|
42
|
+
return celery_viewset_client(celery_app, task_prefix, redis_client=redis_client)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def check_result_readers() -> frozenset[str]:
|
|
46
|
+
"""
|
|
47
|
+
Verify that every queue_key registered by a celery_viewset_client decorator (client/FastAPI
|
|
48
|
+
mode only) has a running result reader task, logging a warning for each one that doesn't.
|
|
49
|
+
|
|
50
|
+
celery_viewset_client decorators register their queue_key at import/decoration time, so by the
|
|
51
|
+
time FastAPI startup code runs, every queue_key in use is already known - call this once, right
|
|
52
|
+
after starting all result readers in the FastAPI lifespan. A queue_key with no running reader
|
|
53
|
+
means requests dispatched to that celery_viewset hang forever waiting for a result that never
|
|
54
|
+
arrives (the Celery task itself runs fine - only the FastAPI-side pickup is missing).
|
|
55
|
+
|
|
56
|
+
Returns the set of queue_keys that are missing a reader (empty if everything is running).
|
|
57
|
+
Never raises - this is a diagnostic aid, not a correctness check enforced by the library.
|
|
58
|
+
"""
|
|
59
|
+
missing = get_registered_queue_keys() - get_running_queue_keys()
|
|
60
|
+
for queue_key in sorted(missing):
|
|
61
|
+
logger.warning(
|
|
62
|
+
"No result reader is running for queue_key=%s - requests dispatched to this "
|
|
63
|
+
"celery_viewset will hang waiting for a result that never arrives. Call "
|
|
64
|
+
"start_result_reader(redis_client, %r) in your FastAPI lifespan.",
|
|
65
|
+
queue_key, queue_key,
|
|
66
|
+
)
|
|
67
|
+
return frozenset(missing)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
__all__ = [
|
|
71
|
+
"celery_viewset",
|
|
72
|
+
"celery_viewset_client",
|
|
73
|
+
"celery_viewset_server",
|
|
74
|
+
"set_is_celery_worker",
|
|
75
|
+
"start_result_reader",
|
|
76
|
+
"stop_result_reader",
|
|
77
|
+
"check_result_readers",
|
|
78
|
+
"push_result",
|
|
79
|
+
"get_result_queue_key",
|
|
80
|
+
"get_registered_queue_keys",
|
|
81
|
+
"get_running_queue_keys",
|
|
82
|
+
]
|
|
@@ -21,6 +21,15 @@ if TYPE_CHECKING:
|
|
|
21
21
|
T = TypeVar("T")
|
|
22
22
|
logger = logging.getLogger(__name__)
|
|
23
23
|
|
|
24
|
+
# queue_keys registered by celery_viewset_client so far - populated at decoration (import) time,
|
|
25
|
+
# used by check_result_readers() to spot a queue_key with no running result reader.
|
|
26
|
+
_registered_queue_keys: set[str] = set()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_registered_queue_keys() -> frozenset[str]:
|
|
30
|
+
"""Return the queue_keys registered by celery_viewset_client decorators applied so far."""
|
|
31
|
+
return frozenset(_registered_queue_keys)
|
|
32
|
+
|
|
24
33
|
|
|
25
34
|
def celery_viewset_client(
|
|
26
35
|
celery_app: "Celery",
|
|
@@ -53,6 +62,7 @@ def celery_viewset_client(
|
|
|
53
62
|
"queue_key": queue_key,
|
|
54
63
|
"mode": "client",
|
|
55
64
|
}
|
|
65
|
+
_registered_queue_keys.add(queue_key)
|
|
56
66
|
|
|
57
67
|
return cls
|
|
58
68
|
|
|
@@ -14,8 +14,8 @@ logger = logging.getLogger(__name__)
|
|
|
14
14
|
# Global registry: correlation_id -> asyncio.Future
|
|
15
15
|
_pending_futures: dict[str, asyncio.Future] = {}
|
|
16
16
|
|
|
17
|
-
# Global reader task
|
|
18
|
-
|
|
17
|
+
# Global reader task registry, keyed by queue_key - one reader per Celery viewset task_prefix
|
|
18
|
+
_reader_tasks: dict[str, asyncio.Task] = {}
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
def register_future(correlation_id: str, future: asyncio.Future) -> None:
|
|
@@ -78,22 +78,39 @@ def push_result(redis_client: "redis.Redis", queue_key: str, correlation_id: str
|
|
|
78
78
|
redis_client.rpush(queue_key, json.dumps(data))
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def get_running_queue_keys() -> frozenset[str]:
|
|
82
|
+
"""Return the queue_keys that currently have a running (not done) reader task."""
|
|
83
|
+
return frozenset(key for key, task in _reader_tasks.items() if not task.done())
|
|
84
|
+
|
|
85
|
+
|
|
81
86
|
async def start_result_reader(redis_client: "redis.Redis", queue_key: str, poll_interval: float = 0.05) -> asyncio.Task:
|
|
82
|
-
"""
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
"""
|
|
88
|
+
Start the background result reader task for the given queue_key.
|
|
89
|
+
|
|
90
|
+
Should be called once per queue_key (i.e. once per celery_viewset task_prefix) on FastAPI
|
|
91
|
+
startup. Calling it again with the same queue_key while its reader is still running returns
|
|
92
|
+
the existing task; a different queue_key gets its own independent reader task.
|
|
93
|
+
"""
|
|
94
|
+
existing = _reader_tasks.get(queue_key)
|
|
95
|
+
if existing is not None and not existing.done():
|
|
96
|
+
return existing
|
|
97
|
+
task = asyncio.create_task(result_reader_loop(redis_client, queue_key, poll_interval))
|
|
98
|
+
_reader_tasks[queue_key] = task
|
|
99
|
+
return task
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def stop_result_reader(queue_key: str | None = None) -> None:
|
|
103
|
+
"""
|
|
104
|
+
Stop background result reader task(s). Should be called on FastAPI shutdown.
|
|
105
|
+
|
|
106
|
+
If queue_key is given, stop only that reader; otherwise stop all running readers.
|
|
107
|
+
"""
|
|
108
|
+
keys = [queue_key] if queue_key is not None else list(_reader_tasks.keys())
|
|
109
|
+
for key in keys:
|
|
110
|
+
task = _reader_tasks.pop(key, None)
|
|
111
|
+
if task is not None and not task.done():
|
|
112
|
+
task.cancel()
|
|
113
|
+
try:
|
|
114
|
+
await task
|
|
115
|
+
except asyncio.CancelledError:
|
|
116
|
+
pass
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.4"
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
|
|
3
|
-
from ..lifecycle_runner import LifecycleType
|
|
4
|
-
from .client import celery_viewset_client
|
|
5
|
-
from .result_reader import get_result_queue_key, push_result, start_result_reader, stop_result_reader
|
|
6
|
-
from .server import celery_viewset_server
|
|
7
|
-
|
|
8
|
-
# Allow explicit override via set_is_celery_worker()
|
|
9
|
-
_is_celery_worker: bool | None = None
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def set_is_celery_worker(is_worker: bool) -> None:
|
|
13
|
-
"""Explicitly set whether we are running in a Celery worker context."""
|
|
14
|
-
global _is_celery_worker
|
|
15
|
-
_is_celery_worker = is_worker
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _detect_is_celery_worker() -> bool:
|
|
19
|
-
"""Auto-detect if we are running inside a Celery worker by inspecting sys.argv."""
|
|
20
|
-
if _is_celery_worker is not None:
|
|
21
|
-
return _is_celery_worker
|
|
22
|
-
return len(sys.argv) > 0 and "celery" in sys.argv[0].lower()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def celery_viewset(celery_app, task_prefix: str, lifecycle: LifecycleType = "singleton", redis_client=None):
|
|
26
|
-
"""
|
|
27
|
-
Convenience decorator that auto-detects whether to use client or server mode.
|
|
28
|
-
|
|
29
|
-
- In a Celery worker (detected via sys.argv or set_is_celery_worker): uses celery_viewset_server
|
|
30
|
-
- In a FastAPI app: uses celery_viewset_client
|
|
31
|
-
|
|
32
|
-
For explicit control, use celery_viewset_client or celery_viewset_server directly.
|
|
33
|
-
"""
|
|
34
|
-
if _detect_is_celery_worker():
|
|
35
|
-
return celery_viewset_server(celery_app, task_prefix, lifecycle=lifecycle, redis_client=redis_client)
|
|
36
|
-
else:
|
|
37
|
-
if redis_client is None:
|
|
38
|
-
raise ValueError("redis_client is required for celery_viewset in client (FastAPI) mode")
|
|
39
|
-
return celery_viewset_client(celery_app, task_prefix, redis_client=redis_client)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
__all__ = [
|
|
43
|
-
"celery_viewset",
|
|
44
|
-
"celery_viewset_client",
|
|
45
|
-
"celery_viewset_server",
|
|
46
|
-
"set_is_celery_worker",
|
|
47
|
-
"start_result_reader",
|
|
48
|
-
"stop_result_reader",
|
|
49
|
-
"push_result",
|
|
50
|
-
"get_result_queue_key",
|
|
51
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dynamicforms_fastapi_viewsets-0.3.4 → dynamicforms_fastapi_viewsets-0.3.5}/fastapi_viewsets/conf.py
RENAMED
|
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
|