jararaca 0.3.11a16__py3-none-any.whl → 0.3.12__py3-none-any.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 jararaca might be problematic. Click here for more details.
- README.md +120 -0
- jararaca/__init__.py +106 -8
- jararaca/cli.py +216 -31
- jararaca/messagebus/worker.py +749 -272
- jararaca/microservice.py +42 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +82 -73
- jararaca/persistence/interceptors/constants.py +1 -0
- jararaca/persistence/interceptors/decorators.py +45 -0
- jararaca/presentation/server.py +57 -11
- jararaca/presentation/websocket/redis.py +113 -7
- jararaca/reflect/metadata.py +1 -1
- jararaca/rpc/http/__init__.py +97 -0
- jararaca/rpc/http/backends/__init__.py +10 -0
- jararaca/rpc/http/backends/httpx.py +39 -9
- jararaca/rpc/http/decorators.py +302 -6
- jararaca/scheduler/beat_worker.py +550 -91
- jararaca/tools/typescript/__init__.py +0 -0
- jararaca/tools/typescript/decorators.py +95 -0
- jararaca/tools/typescript/interface_parser.py +699 -156
- jararaca-0.3.12.dist-info/LICENSE +674 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/METADATA +4 -3
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/RECORD +26 -19
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/WHEEL +1 -1
- pyproject.toml +86 -0
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.3.12.dist-info}/entry_points.txt +0 -0
jararaca/microservice.py
CHANGED
|
@@ -325,6 +325,48 @@ def provide_container(container: Container) -> Generator[None, None, None]:
|
|
|
325
325
|
current_container_ctx.reset(token)
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
class ShutdownState(Protocol):
|
|
329
|
+
|
|
330
|
+
def request_shutdown(self) -> None: ...
|
|
331
|
+
|
|
332
|
+
def is_shutdown_requested(self) -> bool: ...
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def is_shutting_down() -> bool:
|
|
339
|
+
"""
|
|
340
|
+
Check if the application is in the process of shutting down.
|
|
341
|
+
"""
|
|
342
|
+
return shutdown_state_ctx.get().is_shutdown_requested()
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def request_shutdown() -> None:
|
|
346
|
+
"""
|
|
347
|
+
Request the application to shut down.
|
|
348
|
+
This will set the shutdown event, allowing the application to gracefully shut down.
|
|
349
|
+
"""
|
|
350
|
+
shutdown_state_ctx.get().request_shutdown()
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
@contextmanager
|
|
354
|
+
def provide_shutdown_state(
|
|
355
|
+
state: ShutdownState,
|
|
356
|
+
) -> Generator[None, None, None]:
|
|
357
|
+
"""
|
|
358
|
+
Context manager to provide the shutdown state.
|
|
359
|
+
This is used to manage the shutdown event for the application.
|
|
360
|
+
"""
|
|
361
|
+
|
|
362
|
+
token = shutdown_state_ctx.set(state)
|
|
363
|
+
try:
|
|
364
|
+
yield
|
|
365
|
+
finally:
|
|
366
|
+
with suppress(ValueError):
|
|
367
|
+
shutdown_state_ctx.reset(token)
|
|
368
|
+
|
|
369
|
+
|
|
328
370
|
__all__ = [
|
|
329
371
|
"AppTransactionContext",
|
|
330
372
|
"AppInterceptor",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from contextlib import asynccontextmanager, contextmanager, suppress
|
|
2
2
|
from contextvars import ContextVar
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, AsyncGenerator, Generator
|
|
4
|
+
from typing import Any, AsyncGenerator, Generator, Protocol
|
|
5
5
|
|
|
6
6
|
from sqlalchemy.ext.asyncio import (
|
|
7
7
|
AsyncSession,
|
|
@@ -12,9 +12,47 @@ from sqlalchemy.ext.asyncio import (
|
|
|
12
12
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
13
13
|
|
|
14
14
|
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
15
|
-
from jararaca.
|
|
15
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
16
|
+
from jararaca.persistence.interceptors.decorators import (
|
|
17
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE,
|
|
18
|
+
)
|
|
19
|
+
from jararaca.reflect.metadata import get_metadata_value
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionManager(Protocol):
|
|
23
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession: ...
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ctx_session_manager: ContextVar[SessionManager | None] = ContextVar(
|
|
27
|
+
"ctx_session_manager", default=None
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@contextmanager
|
|
32
|
+
def providing_session_manager(
|
|
33
|
+
session_manager: SessionManager,
|
|
34
|
+
) -> Generator[None, Any, None]:
|
|
35
|
+
"""
|
|
36
|
+
Context manager to provide a session manager for the current context.
|
|
37
|
+
"""
|
|
38
|
+
token = ctx_session_manager.set(session_manager)
|
|
39
|
+
try:
|
|
40
|
+
yield
|
|
41
|
+
finally:
|
|
42
|
+
with suppress(ValueError):
|
|
43
|
+
ctx_session_manager.reset(token)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def use_session_manager() -> SessionManager:
|
|
47
|
+
"""
|
|
48
|
+
Retrieve the current session manager from the context variable.
|
|
49
|
+
Raises ValueError if no session manager is set.
|
|
50
|
+
"""
|
|
51
|
+
session_manager = ctx_session_manager.get()
|
|
52
|
+
if session_manager is None:
|
|
53
|
+
raise ValueError("No session manager set in the context.")
|
|
54
|
+
return session_manager
|
|
16
55
|
|
|
17
|
-
DEFAULT_CONNECTION_NAME = "default"
|
|
18
56
|
|
|
19
57
|
ctx_default_connection_name: ContextVar[str] = ContextVar(
|
|
20
58
|
"ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
|
|
@@ -69,13 +107,21 @@ async def providing_new_session(
|
|
|
69
107
|
connection_name: str | None = None,
|
|
70
108
|
) -> AsyncGenerator[AsyncSession, None]:
|
|
71
109
|
|
|
72
|
-
|
|
110
|
+
session_manager = use_session_manager()
|
|
111
|
+
current_session = session_manager.spawn_session(connection_name)
|
|
73
112
|
|
|
74
113
|
async with AsyncSession(
|
|
75
114
|
current_session.bind,
|
|
76
115
|
) as new_session, new_session.begin() as new_tx:
|
|
77
116
|
with providing_session(new_session, new_tx, connection_name):
|
|
78
|
-
|
|
117
|
+
try:
|
|
118
|
+
yield new_session
|
|
119
|
+
if new_tx.is_active:
|
|
120
|
+
await new_tx.commit()
|
|
121
|
+
except Exception:
|
|
122
|
+
if new_tx.is_active:
|
|
123
|
+
await new_tx.rollback()
|
|
124
|
+
raise
|
|
79
125
|
|
|
80
126
|
|
|
81
127
|
def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
@@ -129,50 +175,7 @@ class AIOSQAConfig:
|
|
|
129
175
|
self.inject_default = inject_default
|
|
130
176
|
|
|
131
177
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def set_inject_connection(
|
|
136
|
-
inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
|
|
137
|
-
) -> SetMetadata:
|
|
138
|
-
"""
|
|
139
|
-
Set whether to inject the connection metadata for the given connection name.
|
|
140
|
-
This is useful when you want to control whether the connection metadata
|
|
141
|
-
should be injected into the context or not.
|
|
142
|
-
"""
|
|
143
|
-
|
|
144
|
-
return SetMetadata(
|
|
145
|
-
INJECT_CONNECTION_METADATA.format(connection_name=connection_name), inject
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def uses_connection(
|
|
150
|
-
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
151
|
-
) -> SetMetadata:
|
|
152
|
-
"""
|
|
153
|
-
Use connection metadata for the given connection name.
|
|
154
|
-
This is useful when you want to inject the connection metadata into the context,
|
|
155
|
-
for example, when you are using a specific connection for a specific operation.
|
|
156
|
-
"""
|
|
157
|
-
return SetMetadata(
|
|
158
|
-
INJECT_CONNECTION_METADATA.format(connection_name=connection_name), True
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def dnt_uses_connection(
|
|
163
|
-
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
164
|
-
) -> SetMetadata:
|
|
165
|
-
"""
|
|
166
|
-
Do not use connection metadata for the given connection name.
|
|
167
|
-
This is useful when you want to ensure that the connection metadata is not injected
|
|
168
|
-
into the context, for example, when you are using a different connection for a specific operation.
|
|
169
|
-
"""
|
|
170
|
-
return SetMetadata(
|
|
171
|
-
INJECT_CONNECTION_METADATA.format(connection_name=connection_name), False
|
|
172
|
-
)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
178
|
+
class AIOSqlAlchemySessionInterceptor(AppInterceptor, SessionManager):
|
|
176
179
|
|
|
177
180
|
def __init__(self, config: AIOSQAConfig):
|
|
178
181
|
self.config = config
|
|
@@ -189,27 +192,33 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
189
192
|
self, app_context: AppTransactionContext
|
|
190
193
|
) -> AsyncGenerator[None, None]:
|
|
191
194
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
195
|
+
with providing_session_manager(self):
|
|
196
|
+
uses_connection_metadata = get_metadata_value(
|
|
197
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
198
|
+
connection_name=self.config.connection_name
|
|
199
|
+
),
|
|
200
|
+
self.config.inject_default,
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
if not uses_connection_metadata:
|
|
204
|
+
yield
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
async with self.sessionmaker() as session, session.begin() as tx:
|
|
208
|
+
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
209
|
+
with providing_session(session, tx, self.config.connection_name):
|
|
210
|
+
try:
|
|
211
|
+
yield
|
|
212
|
+
if tx.is_active:
|
|
213
|
+
await tx.commit()
|
|
214
|
+
except Exception as e:
|
|
215
|
+
await tx.rollback()
|
|
216
|
+
raise e
|
|
217
|
+
finally:
|
|
218
|
+
with suppress(ValueError):
|
|
219
|
+
ctx_default_connection_name.reset(token)
|
|
220
|
+
|
|
221
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession:
|
|
222
|
+
connection_name = ensure_name(connection_name)
|
|
223
|
+
session = self.sessionmaker()
|
|
224
|
+
return session
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DEFAULT_CONNECTION_NAME = "default"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
2
|
+
from jararaca.reflect.metadata import SetMetadata
|
|
3
|
+
|
|
4
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE = (
|
|
5
|
+
"inject_persistence_template_{connection_name}"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def set_use_persistence_session(
|
|
10
|
+
inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
|
|
11
|
+
) -> SetMetadata:
|
|
12
|
+
"""
|
|
13
|
+
Set whether to inject the connection metadata for the given connection name.
|
|
14
|
+
This is useful when you want to control whether the connection metadata
|
|
15
|
+
should be injected into the context or not.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
return SetMetadata(
|
|
19
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
20
|
+
connection_name=connection_name
|
|
21
|
+
),
|
|
22
|
+
inject,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def uses_persistence_session(
|
|
27
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
28
|
+
) -> SetMetadata:
|
|
29
|
+
"""
|
|
30
|
+
Use connection metadata for the given connection name.
|
|
31
|
+
This is useful when you want to inject the connection metadata into the context,
|
|
32
|
+
for example, when you are using a specific connection for a specific operation.
|
|
33
|
+
"""
|
|
34
|
+
return set_use_persistence_session(True, connection_name=connection_name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def skip_persistence_session(
|
|
38
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
39
|
+
) -> SetMetadata:
|
|
40
|
+
"""
|
|
41
|
+
Decorator to skip using connection metadata for the given connection name.
|
|
42
|
+
This is useful when you want to ensure that the connection metadata is not injected
|
|
43
|
+
into the context, for example, when you are using a different connection for a specific operation.
|
|
44
|
+
"""
|
|
45
|
+
return set_use_persistence_session(False, connection_name=connection_name)
|
jararaca/presentation/server.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import signal
|
|
3
|
+
import threading
|
|
1
4
|
from contextlib import asynccontextmanager
|
|
5
|
+
from signal import SIGINT, SIGTERM
|
|
2
6
|
from typing import Any, AsyncGenerator
|
|
3
7
|
|
|
4
8
|
from fastapi import Depends, FastAPI, Request, WebSocket
|
|
@@ -10,7 +14,9 @@ from jararaca.lifecycle import AppLifecycle
|
|
|
10
14
|
from jararaca.microservice import (
|
|
11
15
|
AppTransactionContext,
|
|
12
16
|
HttpTransactionData,
|
|
17
|
+
ShutdownState,
|
|
13
18
|
WebSocketTransactionData,
|
|
19
|
+
provide_shutdown_state,
|
|
14
20
|
)
|
|
15
21
|
from jararaca.presentation.decorators import RestController
|
|
16
22
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
@@ -76,10 +82,49 @@ class HttpAppLifecycle:
|
|
|
76
82
|
yield
|
|
77
83
|
|
|
78
84
|
|
|
85
|
+
class HttpShutdownState(ShutdownState):
|
|
86
|
+
def __init__(self) -> None:
|
|
87
|
+
self._requested = False
|
|
88
|
+
self.old_signal_handlers = {
|
|
89
|
+
SIGINT: signal.getsignal(SIGINT),
|
|
90
|
+
SIGTERM: signal.getsignal(SIGTERM),
|
|
91
|
+
}
|
|
92
|
+
self.thread_lock = threading.Lock()
|
|
93
|
+
|
|
94
|
+
def request_shutdown(self) -> None:
|
|
95
|
+
if not self._requested:
|
|
96
|
+
self._requested = True
|
|
97
|
+
os.kill(os.getpid(), SIGINT)
|
|
98
|
+
|
|
99
|
+
def is_shutdown_requested(self) -> bool:
|
|
100
|
+
return self._requested
|
|
101
|
+
|
|
102
|
+
def handle_signal(self, signum: int, frame: Any) -> None:
|
|
103
|
+
print(f"Received signal {signum}, initiating shutdown...")
|
|
104
|
+
if self._requested:
|
|
105
|
+
print("Shutdown already requested, ignoring signal.")
|
|
106
|
+
return
|
|
107
|
+
print("Requesting shutdown...")
|
|
108
|
+
self._requested = True
|
|
109
|
+
|
|
110
|
+
# remove the signal handler to prevent recursion
|
|
111
|
+
for sig in (SIGINT, SIGTERM):
|
|
112
|
+
if self.old_signal_handlers[sig] is not None:
|
|
113
|
+
signal.signal(sig, self.old_signal_handlers[sig])
|
|
114
|
+
|
|
115
|
+
signal.raise_signal(signum)
|
|
116
|
+
|
|
117
|
+
def setup_signal_handlers(self) -> None:
|
|
118
|
+
signal.signal(SIGINT, self.handle_signal)
|
|
119
|
+
signal.signal(SIGTERM, self.handle_signal)
|
|
120
|
+
|
|
121
|
+
|
|
79
122
|
class HttpUowContextProviderDependency:
|
|
80
123
|
|
|
81
124
|
def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
|
|
82
125
|
self.uow_provider = uow_provider
|
|
126
|
+
self.shutdown_state = HttpShutdownState()
|
|
127
|
+
self.shutdown_state.setup_signal_handlers()
|
|
83
128
|
|
|
84
129
|
async def __call__(
|
|
85
130
|
self, websocket: WebSocket = None, request: Request = None # type: ignore
|
|
@@ -101,17 +146,18 @@ class HttpUowContextProviderDependency:
|
|
|
101
146
|
"ControllerMemberReflect, but got: {}".format(type(member))
|
|
102
147
|
)
|
|
103
148
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
with provide_shutdown_state(self.shutdown_state):
|
|
150
|
+
async with self.uow_provider(
|
|
151
|
+
AppTransactionContext(
|
|
152
|
+
controller_member_reflect=member,
|
|
153
|
+
transaction_data=(
|
|
154
|
+
HttpTransactionData(request=request)
|
|
155
|
+
if request
|
|
156
|
+
else WebSocketTransactionData(websocket=websocket)
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
):
|
|
160
|
+
yield
|
|
115
161
|
|
|
116
162
|
|
|
117
163
|
def create_http_server(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
2
3
|
from dataclasses import dataclass
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
@@ -10,6 +11,8 @@ from jararaca.presentation.websocket.websocket_interceptor import (
|
|
|
10
11
|
WebSocketConnectionBackend,
|
|
11
12
|
)
|
|
12
13
|
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
@dataclass
|
|
15
18
|
class BroadcastMessage:
|
|
@@ -55,6 +58,7 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
|
|
|
55
58
|
send_pubsub_channel: str,
|
|
56
59
|
consume_broadcast_timeout: int = 1,
|
|
57
60
|
consume_send_timeout: int = 1,
|
|
61
|
+
retry_delay: float = 5.0,
|
|
58
62
|
) -> None:
|
|
59
63
|
|
|
60
64
|
self.redis = conn
|
|
@@ -66,6 +70,35 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
|
|
|
66
70
|
|
|
67
71
|
self.consume_broadcast_timeout = consume_broadcast_timeout
|
|
68
72
|
self.consume_send_timeout = consume_send_timeout
|
|
73
|
+
self.retry_delay = retry_delay
|
|
74
|
+
self.__shutdown_event: asyncio.Event | None = None
|
|
75
|
+
|
|
76
|
+
self.__send_func: SendFunc | None = None
|
|
77
|
+
self.__broadcast_func: BroadcastFunc | None = None
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def shutdown_event(self) -> asyncio.Event:
|
|
81
|
+
if self.__shutdown_event is None:
|
|
82
|
+
raise RuntimeError(
|
|
83
|
+
"Shutdown event is not set. Please configure the backend before using it."
|
|
84
|
+
)
|
|
85
|
+
return self.__shutdown_event
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def send_func(self) -> SendFunc:
|
|
89
|
+
if self.__send_func is None:
|
|
90
|
+
raise RuntimeError(
|
|
91
|
+
"Send function is not set. Please configure the backend before using it."
|
|
92
|
+
)
|
|
93
|
+
return self.__send_func
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def broadcast_func(self) -> BroadcastFunc:
|
|
97
|
+
if self.__broadcast_func is None:
|
|
98
|
+
raise RuntimeError(
|
|
99
|
+
"Broadcast function is not set. Please configure the backend before using it."
|
|
100
|
+
)
|
|
101
|
+
return self.__broadcast_func
|
|
69
102
|
|
|
70
103
|
async def broadcast(self, message: bytes) -> None:
|
|
71
104
|
await self.redis.publish(
|
|
@@ -82,22 +115,95 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
|
|
|
82
115
|
def configure(
|
|
83
116
|
self, broadcast: BroadcastFunc, send: SendFunc, shutdown_event: asyncio.Event
|
|
84
117
|
) -> None:
|
|
118
|
+
if self.__shutdown_event is not None:
|
|
119
|
+
raise RuntimeError("Backend is already configured.")
|
|
120
|
+
self.__shutdown_event = shutdown_event
|
|
121
|
+
self.__send_func = send
|
|
122
|
+
self.__broadcast_func = broadcast
|
|
123
|
+
self.setup_send_consumer()
|
|
124
|
+
self.setup_broadcast_consumer()
|
|
85
125
|
|
|
86
|
-
|
|
87
|
-
self.consume_broadcast(broadcast, shutdown_event)
|
|
88
|
-
)
|
|
126
|
+
def setup_send_consumer(self) -> None:
|
|
89
127
|
|
|
90
128
|
send_task = asyncio.get_event_loop().create_task(
|
|
91
|
-
self.consume_send(
|
|
129
|
+
self.consume_send(self.send_func, self.shutdown_event)
|
|
92
130
|
)
|
|
93
131
|
|
|
94
|
-
self.tasks.add(broadcast_task)
|
|
95
132
|
self.tasks.add(send_task)
|
|
133
|
+
send_task.add_done_callback(self.handle_send_task_done)
|
|
134
|
+
|
|
135
|
+
def setup_broadcast_consumer(self) -> None:
|
|
136
|
+
|
|
137
|
+
broadcast_task = asyncio.get_event_loop().create_task(
|
|
138
|
+
self.consume_broadcast(self.broadcast_func, self.shutdown_event)
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self.tasks.add(broadcast_task)
|
|
142
|
+
|
|
143
|
+
broadcast_task.add_done_callback(self.handle_broadcast_task_done)
|
|
144
|
+
|
|
145
|
+
def handle_broadcast_task_done(self, task: asyncio.Task[Any]) -> None:
|
|
146
|
+
if task.cancelled():
|
|
147
|
+
logger.warning("Broadcast task was cancelled.")
|
|
148
|
+
elif task.exception() is not None:
|
|
149
|
+
logger.exception(
|
|
150
|
+
f"Broadcast task raised an exception:", exc_info=task.exception()
|
|
151
|
+
)
|
|
152
|
+
else:
|
|
153
|
+
logger.warning("Broadcast task somehow completed successfully.")
|
|
154
|
+
|
|
155
|
+
if not self.shutdown_event.is_set():
|
|
156
|
+
logger.warning(
|
|
157
|
+
"Broadcast task completed, but shutdown event is not set. This is unexpected."
|
|
158
|
+
)
|
|
159
|
+
# Add delay before retrying to avoid excessive CPU usage
|
|
160
|
+
asyncio.get_event_loop().create_task(
|
|
161
|
+
self._retry_broadcast_consumer_with_delay()
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def handle_send_task_done(self, task: asyncio.Task[Any]) -> None:
|
|
165
|
+
if task.cancelled():
|
|
166
|
+
logger.warning("Send task was cancelled.")
|
|
167
|
+
elif task.exception() is not None:
|
|
168
|
+
logger.exception(
|
|
169
|
+
f"Send task raised an exception:", exc_info=task.exception()
|
|
170
|
+
)
|
|
171
|
+
else:
|
|
172
|
+
logger.warning("Send task somehow completed successfully.")
|
|
173
|
+
|
|
174
|
+
if not self.shutdown_event.is_set():
|
|
175
|
+
logger.warning(
|
|
176
|
+
"Send task completed, but shutdown event is not set. This is unexpected."
|
|
177
|
+
)
|
|
178
|
+
# Add delay before retrying to avoid excessive CPU usage
|
|
179
|
+
asyncio.get_event_loop().create_task(self._retry_send_consumer_with_delay())
|
|
180
|
+
|
|
181
|
+
async def _retry_broadcast_consumer_with_delay(self) -> None:
|
|
182
|
+
"""Retry setting up broadcast consumer after a delay to avoid excessive CPU usage."""
|
|
183
|
+
logger.info(
|
|
184
|
+
f"Waiting {self.retry_delay} seconds before retrying broadcast consumer..."
|
|
185
|
+
)
|
|
186
|
+
await asyncio.sleep(self.retry_delay)
|
|
187
|
+
|
|
188
|
+
if not self.shutdown_event.is_set():
|
|
189
|
+
logger.info("Retrying broadcast consumer setup...")
|
|
190
|
+
self.setup_broadcast_consumer()
|
|
191
|
+
|
|
192
|
+
async def _retry_send_consumer_with_delay(self) -> None:
|
|
193
|
+
"""Retry setting up send consumer after a delay to avoid excessive CPU usage."""
|
|
194
|
+
logger.info(
|
|
195
|
+
f"Waiting {self.retry_delay} seconds before retrying send consumer..."
|
|
196
|
+
)
|
|
197
|
+
await asyncio.sleep(self.retry_delay)
|
|
198
|
+
|
|
199
|
+
if not self.shutdown_event.is_set():
|
|
200
|
+
logger.info("Retrying send consumer setup...")
|
|
201
|
+
self.setup_send_consumer()
|
|
96
202
|
|
|
97
203
|
async def consume_broadcast(
|
|
98
204
|
self, broadcast: BroadcastFunc, shutdown_event: asyncio.Event
|
|
99
205
|
) -> None:
|
|
100
|
-
|
|
206
|
+
logger.info("Starting broadcast consumer...")
|
|
101
207
|
async with self.redis.pubsub() as pubsub:
|
|
102
208
|
await pubsub.subscribe(self.broadcast_pubsub_channel)
|
|
103
209
|
|
|
@@ -122,7 +228,7 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
|
|
|
122
228
|
task.add_done_callback(self.tasks.discard)
|
|
123
229
|
|
|
124
230
|
async def consume_send(self, send: SendFunc, shutdown_event: asyncio.Event) -> None:
|
|
125
|
-
|
|
231
|
+
logger.info("Starting send consumer...")
|
|
126
232
|
async with self.redis.pubsub() as pubsub:
|
|
127
233
|
await pubsub.subscribe(self.send_pubsub_channel)
|
|
128
234
|
|
jararaca/reflect/metadata.py
CHANGED
|
@@ -6,7 +6,7 @@ from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
|
|
|
6
6
|
DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
@dataclass
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
10
|
class ControllerInstanceMetadata:
|
|
11
11
|
value: Any
|
|
12
12
|
inherited: bool
|
jararaca/rpc/http/__init__.py
CHANGED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# HTTP RPC Module - Complete REST Client Implementation
|
|
2
|
+
"""
|
|
3
|
+
This module provides a complete REST client implementation with support for:
|
|
4
|
+
- HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
|
|
5
|
+
- Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
|
|
6
|
+
- Configuration decorators (@Timeout, @Retry, @ContentType)
|
|
7
|
+
- Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
|
|
8
|
+
- Caching and response middleware
|
|
9
|
+
- Request/response hooks for customization
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .backends.httpx import HTTPXHttpRPCAsyncBackend
|
|
13
|
+
from .decorators import ( # HTTP Method decorators; Request parameter decorators; Configuration decorators; Client builder and core classes; Authentication classes; Middleware and hooks; Configuration classes; Data structures; Error handlers; Exceptions
|
|
14
|
+
ApiKeyAuth,
|
|
15
|
+
AuthenticationMiddleware,
|
|
16
|
+
BasicAuth,
|
|
17
|
+
BearerTokenAuth,
|
|
18
|
+
Body,
|
|
19
|
+
CacheMiddleware,
|
|
20
|
+
ContentType,
|
|
21
|
+
Delete,
|
|
22
|
+
File,
|
|
23
|
+
FormData,
|
|
24
|
+
Get,
|
|
25
|
+
GlobalHttpErrorHandler,
|
|
26
|
+
Header,
|
|
27
|
+
HttpMapping,
|
|
28
|
+
HttpRpcClientBuilder,
|
|
29
|
+
HttpRPCRequest,
|
|
30
|
+
HttpRPCResponse,
|
|
31
|
+
Patch,
|
|
32
|
+
PathParam,
|
|
33
|
+
Post,
|
|
34
|
+
Put,
|
|
35
|
+
Query,
|
|
36
|
+
RequestAttribute,
|
|
37
|
+
RequestHook,
|
|
38
|
+
ResponseHook,
|
|
39
|
+
ResponseMiddleware,
|
|
40
|
+
RestClient,
|
|
41
|
+
Retry,
|
|
42
|
+
RetryConfig,
|
|
43
|
+
RouteHttpErrorHandler,
|
|
44
|
+
RPCRequestNetworkError,
|
|
45
|
+
RPCUnhandleError,
|
|
46
|
+
Timeout,
|
|
47
|
+
TimeoutException,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
# HTTP Method decorators
|
|
52
|
+
"Get",
|
|
53
|
+
"Post",
|
|
54
|
+
"Put",
|
|
55
|
+
"Patch",
|
|
56
|
+
"Delete",
|
|
57
|
+
# Request parameter decorators
|
|
58
|
+
"Query",
|
|
59
|
+
"Header",
|
|
60
|
+
"PathParam",
|
|
61
|
+
"Body",
|
|
62
|
+
"FormData",
|
|
63
|
+
"File",
|
|
64
|
+
# Configuration decorators
|
|
65
|
+
"Timeout",
|
|
66
|
+
"Retry",
|
|
67
|
+
"ContentType",
|
|
68
|
+
# Client builder and core classes
|
|
69
|
+
"RestClient",
|
|
70
|
+
"HttpRpcClientBuilder",
|
|
71
|
+
"HttpMapping",
|
|
72
|
+
"RequestAttribute",
|
|
73
|
+
# Authentication classes
|
|
74
|
+
"BearerTokenAuth",
|
|
75
|
+
"BasicAuth",
|
|
76
|
+
"ApiKeyAuth",
|
|
77
|
+
"AuthenticationMiddleware",
|
|
78
|
+
# Middleware and hooks
|
|
79
|
+
"CacheMiddleware",
|
|
80
|
+
"ResponseMiddleware",
|
|
81
|
+
"RequestHook",
|
|
82
|
+
"ResponseHook",
|
|
83
|
+
# Configuration classes
|
|
84
|
+
"RetryConfig",
|
|
85
|
+
# Data structures
|
|
86
|
+
"HttpRPCRequest",
|
|
87
|
+
"HttpRPCResponse",
|
|
88
|
+
# Error handlers
|
|
89
|
+
"GlobalHttpErrorHandler",
|
|
90
|
+
"RouteHttpErrorHandler",
|
|
91
|
+
# Exceptions
|
|
92
|
+
"RPCRequestNetworkError",
|
|
93
|
+
"RPCUnhandleError",
|
|
94
|
+
"TimeoutException",
|
|
95
|
+
# Backend
|
|
96
|
+
"HTTPXHttpRPCAsyncBackend",
|
|
97
|
+
]
|