rappel 0.4.1__py3-none-win_amd64.whl → 0.4.5__py3-none-win_amd64.whl
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.
Potentially problematic release.
This version of rappel might be problematic. Click here for more details.
- rappel/actions.py +31 -4
- rappel/bin/boot-rappel-singleton.exe +0 -0
- rappel/bin/rappel-bridge.exe +0 -0
- rappel/bin/start-workers.exe +0 -0
- rappel/dependencies.py +21 -7
- rappel/registry.py +33 -2
- {rappel-0.4.1.data → rappel-0.4.5.data}/scripts/boot-rappel-singleton.exe +0 -0
- {rappel-0.4.1.data → rappel-0.4.5.data}/scripts/rappel-bridge.exe +0 -0
- {rappel-0.4.1.data → rappel-0.4.5.data}/scripts/start-workers.exe +0 -0
- {rappel-0.4.1.dist-info → rappel-0.4.5.dist-info}/METADATA +12 -5
- {rappel-0.4.1.dist-info → rappel-0.4.5.dist-info}/RECORD +13 -13
- {rappel-0.4.1.dist-info → rappel-0.4.5.dist-info}/WHEEL +0 -0
- {rappel-0.4.1.dist-info → rappel-0.4.5.dist-info}/entry_points.txt +0 -0
rappel/actions.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from functools import wraps
|
|
3
4
|
from typing import Any, Callable, Optional, TypeVar, overload
|
|
4
5
|
|
|
5
6
|
from proto import messages_pb2 as pb2
|
|
6
7
|
|
|
8
|
+
from .dependencies import provide_dependencies
|
|
7
9
|
from .registry import AsyncAction, registry
|
|
8
10
|
from .serialization import dumps, loads
|
|
9
11
|
|
|
@@ -64,17 +66,42 @@ def action(
|
|
|
64
66
|
*,
|
|
65
67
|
name: Optional[str] = None,
|
|
66
68
|
) -> Callable[[TAsync], TAsync] | TAsync:
|
|
67
|
-
"""Decorator for registering async actions.
|
|
69
|
+
"""Decorator for registering async actions.
|
|
70
|
+
|
|
71
|
+
Actions decorated with @action will automatically resolve Depend() markers
|
|
72
|
+
when called directly (e.g., during pytest runs where workflows bypass the
|
|
73
|
+
gRPC bridge).
|
|
74
|
+
"""
|
|
68
75
|
|
|
69
76
|
def decorator(target: TAsync) -> TAsync:
|
|
70
77
|
if not inspect.iscoroutinefunction(target):
|
|
71
78
|
raise TypeError(f"action '{target.__name__}' must be defined with 'async def'")
|
|
72
79
|
action_name = name or target.__name__
|
|
73
80
|
action_module = target.__module__
|
|
81
|
+
|
|
82
|
+
@wraps(target)
|
|
83
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
84
|
+
# Convert positional args to kwargs based on the signature
|
|
85
|
+
sig = inspect.signature(target)
|
|
86
|
+
params = list(sig.parameters.keys())
|
|
87
|
+
for i, arg in enumerate(args):
|
|
88
|
+
if i < len(params):
|
|
89
|
+
kwargs[params[i]] = arg
|
|
90
|
+
|
|
91
|
+
# Resolve dependencies using the same mechanism as execute_action
|
|
92
|
+
async with provide_dependencies(target, kwargs) as call_kwargs:
|
|
93
|
+
return await target(**call_kwargs)
|
|
94
|
+
|
|
95
|
+
# Copy over the original function's attributes for introspection
|
|
96
|
+
wrapper.__wrapped__ = target # type: ignore[attr-defined]
|
|
97
|
+
wrapper.__rappel_action_name__ = action_name # type: ignore[attr-defined]
|
|
98
|
+
wrapper.__rappel_action_module__ = action_module # type: ignore[attr-defined]
|
|
99
|
+
|
|
100
|
+
# Register the original function (not the wrapper) so execute_action
|
|
101
|
+
# doesn't double-resolve dependencies
|
|
74
102
|
registry.register(action_module, action_name, target)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return target
|
|
103
|
+
|
|
104
|
+
return wrapper # type: ignore[return-value]
|
|
78
105
|
|
|
79
106
|
if func is not None:
|
|
80
107
|
return decorator(func)
|
|
Binary file
|
rappel/bin/rappel-bridge.exe
CHANGED
|
Binary file
|
rappel/bin/start-workers.exe
CHANGED
|
Binary file
|
rappel/dependencies.py
CHANGED
|
@@ -7,26 +7,40 @@ from typing import Annotated, Any, AsyncIterator, Callable, Optional, get_args,
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@dataclass(frozen=True)
|
|
10
|
-
class
|
|
11
|
-
"""
|
|
10
|
+
class DependMarker:
|
|
11
|
+
"""Internal marker for dependency injection."""
|
|
12
12
|
|
|
13
13
|
dependency: Optional[Callable[..., Any]] = None
|
|
14
14
|
use_cache: bool = True
|
|
15
15
|
|
|
16
16
|
|
|
17
|
-
def
|
|
17
|
+
def Depend( # noqa: N802
|
|
18
|
+
dependency: Optional[Callable[..., Any]] = None,
|
|
19
|
+
*,
|
|
20
|
+
use_cache: bool = True,
|
|
21
|
+
) -> Any:
|
|
22
|
+
"""Marker for dependency injection, mirroring FastAPI's Depends syntax.
|
|
23
|
+
|
|
24
|
+
Returns Any to allow usage as a default parameter value:
|
|
25
|
+
def my_func(service: MyService = Depend(get_service)):
|
|
26
|
+
...
|
|
27
|
+
"""
|
|
28
|
+
return DependMarker(dependency=dependency, use_cache=use_cache)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _depend_from_annotation(annotation: Any) -> DependMarker | None:
|
|
18
32
|
origin = get_origin(annotation)
|
|
19
33
|
if origin is not Annotated:
|
|
20
34
|
return None
|
|
21
35
|
metadata = get_args(annotation)[1:]
|
|
22
36
|
for meta in metadata:
|
|
23
|
-
if isinstance(meta,
|
|
37
|
+
if isinstance(meta, DependMarker):
|
|
24
38
|
return meta
|
|
25
39
|
return None
|
|
26
40
|
|
|
27
41
|
|
|
28
|
-
def _dependency_marker(parameter: inspect.Parameter) ->
|
|
29
|
-
if isinstance(parameter.default,
|
|
42
|
+
def _dependency_marker(parameter: inspect.Parameter) -> DependMarker | None:
|
|
43
|
+
if isinstance(parameter.default, DependMarker):
|
|
30
44
|
return parameter.default
|
|
31
45
|
return _depend_from_annotation(parameter.annotation)
|
|
32
46
|
|
|
@@ -69,7 +83,7 @@ class _DependencyResolver:
|
|
|
69
83
|
raise TypeError(f"Missing required parameter '{name}' for {func_name}")
|
|
70
84
|
return call_kwargs
|
|
71
85
|
|
|
72
|
-
async def _resolve_dependency(self, marker:
|
|
86
|
+
async def _resolve_dependency(self, marker: DependMarker) -> Any:
|
|
73
87
|
dependency = marker.dependency
|
|
74
88
|
if dependency is None:
|
|
75
89
|
raise TypeError("Depend requires a dependency callable")
|
rappel/registry.py
CHANGED
|
@@ -29,6 +29,32 @@ class ActionRegistry:
|
|
|
29
29
|
self._actions: dict[str, _ActionEntry] = {}
|
|
30
30
|
self._lock = RLock()
|
|
31
31
|
|
|
32
|
+
def _source_fingerprint(self, func: AsyncAction) -> tuple[str | None, str | None]:
|
|
33
|
+
func_any: Any = func
|
|
34
|
+
try:
|
|
35
|
+
code = func_any.__code__
|
|
36
|
+
except AttributeError:
|
|
37
|
+
return (None, None)
|
|
38
|
+
try:
|
|
39
|
+
qualname = func_any.__qualname__
|
|
40
|
+
except AttributeError:
|
|
41
|
+
qualname = None
|
|
42
|
+
filename = code.co_filename
|
|
43
|
+
if not isinstance(filename, str):
|
|
44
|
+
filename = None
|
|
45
|
+
if qualname is not None and not isinstance(qualname, str):
|
|
46
|
+
qualname = None
|
|
47
|
+
return (filename, qualname)
|
|
48
|
+
|
|
49
|
+
def _is_same_action_definition(self, existing: AsyncAction, new: AsyncAction) -> bool:
|
|
50
|
+
if existing is new:
|
|
51
|
+
return True
|
|
52
|
+
existing_fingerprint = self._source_fingerprint(existing)
|
|
53
|
+
new_fingerprint = self._source_fingerprint(new)
|
|
54
|
+
if existing_fingerprint == (None, None) or new_fingerprint == (None, None):
|
|
55
|
+
return False
|
|
56
|
+
return existing_fingerprint == new_fingerprint
|
|
57
|
+
|
|
32
58
|
def register(self, module: str, name: str, func: AsyncAction) -> None:
|
|
33
59
|
"""Register an action with its module and name.
|
|
34
60
|
|
|
@@ -38,11 +64,16 @@ class ActionRegistry:
|
|
|
38
64
|
func: The async function to execute.
|
|
39
65
|
|
|
40
66
|
Raises:
|
|
41
|
-
ValueError: If an action with the same module:name is already registered
|
|
67
|
+
ValueError: If an action with the same module:name is already registered
|
|
68
|
+
with a different implementation.
|
|
42
69
|
"""
|
|
43
70
|
key = _make_key(module, name)
|
|
44
71
|
with self._lock:
|
|
45
|
-
|
|
72
|
+
existing = self._actions.get(key)
|
|
73
|
+
if existing is not None:
|
|
74
|
+
if self._is_same_action_definition(existing.func, func):
|
|
75
|
+
self._actions[key] = _ActionEntry(module=module, name=name, func=func)
|
|
76
|
+
return
|
|
46
77
|
raise ValueError(f"action '{module}:{name}' already registered")
|
|
47
78
|
self._actions[key] = _ActionEntry(module=module, name=name, func=func)
|
|
48
79
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rappel
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: Distributed & durable background events in Python
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
6
|
Requires-Dist: googleapis-common-protos>=1.72.0
|
|
@@ -155,19 +155,26 @@ If you have a particular workflow that you think should be working but isn't yet
|
|
|
155
155
|
|
|
156
156
|
The main rappel configuration is done through env vars, which is what you'll typically use in production when using a docker deployment pipeline. If we can't find an environment parameter we will fallback to looking for an .env that specifies it within your local filesystem.
|
|
157
157
|
|
|
158
|
+
These are the primary environment parameters that you'll likely want to customize for your deployment:
|
|
159
|
+
|
|
158
160
|
| Environment Variable | Description | Default | Example |
|
|
159
161
|
|---------------------|-------------|---------|---------|
|
|
160
|
-
| `
|
|
161
|
-
| `RAPPEL_HTTP_ADDR` | HTTP bind address for `rappel-bridge` | `127.0.0.1:24117` | `0.0.0.0:24117` |
|
|
162
|
-
| `RAPPEL_GRPC_ADDR` | gRPC bind address for `rappel-bridge` | HTTP port + 1 | `0.0.0.0:24118` |
|
|
162
|
+
| `RAPPEL_DATABASE_URL` | PostgreSQL connection string for the rappel server | (required on bridge &workers ) | `postgresql://user:pass@localhost:5433/rappel` |
|
|
163
163
|
| `RAPPEL_WORKER_COUNT` | Number of Python worker processes | `num_cpus` | `8` |
|
|
164
164
|
| `RAPPEL_CONCURRENT_PER_WORKER` | Max concurrent actions per worker | `10` | `20` |
|
|
165
165
|
| `RAPPEL_USER_MODULE` | Python module preloaded into each worker | none | `my_app.actions` |
|
|
166
166
|
| `RAPPEL_POLL_INTERVAL_MS` | Poll interval for the dispatch loop (ms) | `100` | `50` |
|
|
167
|
-
| `RAPPEL_BATCH_SIZE` | Max actions fetched per poll | `workers * concurrent_per_worker` | `200` |
|
|
168
167
|
| `RAPPEL_WEBAPP_ENABLED` | Enable the web dashboard | `false` | `true` |
|
|
169
168
|
| `RAPPEL_WEBAPP_ADDR` | Web dashboard bind address | `0.0.0.0:24119` | `0.0.0.0:8080` |
|
|
170
169
|
|
|
170
|
+
We expect that you won't need to modify the following env parameters, but we provide them for convenience:
|
|
171
|
+
|
|
172
|
+
| Environment Variable | Description | Default | Example |
|
|
173
|
+
|---------------------|-------------|---------|---------|
|
|
174
|
+
| `RAPPEL_HTTP_ADDR` | HTTP bind address for `rappel-bridge` | `127.0.0.1:24117` | `0.0.0.0:24117` |
|
|
175
|
+
| `RAPPEL_GRPC_ADDR` | gRPC bind address for `rappel-bridge` | HTTP port + 1 | `0.0.0.0:24118` |
|
|
176
|
+
| `RAPPEL_BATCH_SIZE` | Max actions fetched per poll | `workers * concurrent_per_worker` | `200` |
|
|
177
|
+
|
|
171
178
|
## Philosophy
|
|
172
179
|
|
|
173
180
|
Background jobs in webapps are so frequently used that they should really be a primitive of your fullstack library: database, backend, frontend, _and_ background jobs. Otherwise you're stuck in a situation where users either have to always make blocking requests to an API or you spin up ephemeral tasks that will be killed during re-deployments or an accidental docker crash.
|
|
@@ -7,26 +7,26 @@ proto/messages_pb2.pyi,sha256=lzv_RubGC8ZNVwh70FQr6DI9MECKHbvmVrL0hcBUDPc,39801
|
|
|
7
7
|
proto/messages_pb2_grpc.py,sha256=6MhQxDSSTSiW55qZh7nIRtiEsuBp5t9tCbcZxiuWcF4,16252
|
|
8
8
|
proto/messages_pb2_grpc.pyi,sha256=dAauApqvlu5Ij7JncrOIhKgcQZJEeV8FrdoJydMbfRo,12702
|
|
9
9
|
rappel/__init__.py,sha256=2jnekFrCK42PbFxBiXKLa5wzzYcIy7VQ1zFizv1ZUpo,1391
|
|
10
|
-
rappel/actions.py,sha256=
|
|
10
|
+
rappel/actions.py,sha256=Zf3klKHNNl9UjgEkETMQWbn4wS2Ya-6JGnaRxUo-yW8,3955
|
|
11
11
|
rappel/bridge.py,sha256=t-j6k3rHO84qIoOB_VFCUJ87RHKwDDHejxXkNsfQfi0,8219
|
|
12
|
-
rappel/dependencies.py,sha256=
|
|
12
|
+
rappel/dependencies.py,sha256=OkgxNG1mXMGepxmDMPxBwqd8Y4ebyIMoxOTbb33FQ9M,5789
|
|
13
13
|
rappel/exceptions.py,sha256=mLkNf1a44rbRxuFZp9saiSp0Ec3Y4gXPXI7Ozkhdt1U,348
|
|
14
14
|
rappel/formatter.py,sha256=mNLJ24nkl5zN-zXe_ObpZGeC__Pyv-7J-iPde-wLvbM,3223
|
|
15
15
|
rappel/ir_builder.py,sha256=wBLdp0pndpBSmzLXGIVGZJ6p4k4OlNi30xErY2Z7ZDk,128070
|
|
16
16
|
rappel/logger.py,sha256=auFfr0gdVP9J53OiADhJlCSdS9As-1DA14WNtAqgv3Y,1157
|
|
17
|
-
rappel/registry.py,sha256=
|
|
17
|
+
rappel/registry.py,sha256=xrUNZqeKtwuqKkHzvzCmNsMtO34m-REQBWdcZyIirMQ,3632
|
|
18
18
|
rappel/schedule.py,sha256=k_DD2BZBnJCK60VoOLlIJh6QHIIMiEnSOKsUSRved_I,9464
|
|
19
19
|
rappel/serialization.py,sha256=k4uq_Wka5ptVYp0SIjXD5zflbmgxUV05pNmQs-b5i8o,8028
|
|
20
20
|
rappel/worker.py,sha256=0iZkruFbGBXLLZNmENqxGF4f-ot5v9m0HzWNt8pT24s,7027
|
|
21
21
|
rappel/workflow.py,sha256=Jv6SzPhLxS6oNfiKFCbp7GroWK8rGY1pMd8JqoVEPDM,8270
|
|
22
22
|
rappel/workflow_runtime.py,sha256=GBLaAi1MB7rQmTtL7adAJt9rDLoWS9n52aD1YcJA-ZI,4403
|
|
23
|
-
rappel/bin/boot-rappel-singleton.exe,sha256=
|
|
24
|
-
rappel/bin/rappel-bridge.exe,sha256=
|
|
25
|
-
rappel/bin/start-workers.exe,sha256=
|
|
26
|
-
rappel-0.4.
|
|
27
|
-
rappel-0.4.
|
|
28
|
-
rappel-0.4.
|
|
29
|
-
rappel-0.4.
|
|
30
|
-
rappel-0.4.
|
|
31
|
-
rappel-0.4.
|
|
32
|
-
rappel-0.4.
|
|
23
|
+
rappel/bin/boot-rappel-singleton.exe,sha256=FeJdZiFVoesqrpG8_ESSyXGrVkSDB4dsqdXta51fygI,5104128
|
|
24
|
+
rappel/bin/rappel-bridge.exe,sha256=JpnSY7RYd1-NO3kPfuiS__g6tTMdUjQMe18rmsf-Wls,9680896
|
|
25
|
+
rappel/bin/start-workers.exe,sha256=nCvJWiNIrGAwlxzd0hFVJjI6MEaGDQlRXwPwmjxmj8A,15667712
|
|
26
|
+
rappel-0.4.5.dist-info/METADATA,sha256=ddaik4XZC1PT3HUbybTfnCyu9lUINfi4fd8JmBbcIEA,15738
|
|
27
|
+
rappel-0.4.5.dist-info/entry_points.txt,sha256=h9D-AufOUWpdE7XjnyZyQCc-kER-ZIKj1Jryc1JNL_I,53
|
|
28
|
+
rappel-0.4.5.dist-info/RECORD,,
|
|
29
|
+
rappel-0.4.5.data/scripts/boot-rappel-singleton.exe,sha256=FeJdZiFVoesqrpG8_ESSyXGrVkSDB4dsqdXta51fygI,5104128
|
|
30
|
+
rappel-0.4.5.data/scripts/rappel-bridge.exe,sha256=JpnSY7RYd1-NO3kPfuiS__g6tTMdUjQMe18rmsf-Wls,9680896
|
|
31
|
+
rappel-0.4.5.data/scripts/start-workers.exe,sha256=nCvJWiNIrGAwlxzd0hFVJjI6MEaGDQlRXwPwmjxmj8A,15667712
|
|
32
|
+
rappel-0.4.5.dist-info/WHEEL,sha256=phIoPJnECdbLLKrTiGU1mv92w_v6wBxRMAejLbbrKno,94
|
|
File without changes
|
|
File without changes
|