jararaca 0.3.11a4__py3-none-any.whl → 0.3.11a5__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.
- jararaca/__init__.py +45 -2
- jararaca/cli.py +2 -2
- jararaca/core/uow.py +17 -12
- jararaca/messagebus/decorators.py +31 -30
- jararaca/messagebus/interceptors/publisher_interceptor.py +5 -3
- jararaca/messagebus/worker.py +13 -6
- jararaca/messagebus/worker_v2.py +21 -26
- jararaca/microservice.py +61 -18
- jararaca/observability/decorators.py +7 -3
- jararaca/observability/interceptor.py +4 -2
- jararaca/observability/providers/otel.py +14 -10
- jararaca/persistence/interceptors/aiosqa_interceptor.py +89 -11
- jararaca/presentation/decorators.py +96 -10
- jararaca/presentation/server.py +31 -4
- jararaca/presentation/websocket/websocket_interceptor.py +4 -2
- jararaca/reflect/__init__.py +0 -0
- jararaca/reflect/controller_inspect.py +75 -0
- jararaca/{tools → reflect}/metadata.py +25 -5
- jararaca/scheduler/decorators.py +48 -20
- jararaca/scheduler/scheduler.py +27 -22
- jararaca/scheduler/scheduler_v2.py +20 -16
- jararaca/tools/app_config/interceptor.py +4 -2
- {jararaca-0.3.11a4.dist-info → jararaca-0.3.11a5.dist-info}/METADATA +2 -1
- {jararaca-0.3.11a4.dist-info → jararaca-0.3.11a5.dist-info}/RECORD +27 -25
- {jararaca-0.3.11a4.dist-info → jararaca-0.3.11a5.dist-info}/LICENSE +0 -0
- {jararaca-0.3.11a4.dist-info → jararaca-0.3.11a5.dist-info}/WHEEL +0 -0
- {jararaca-0.3.11a4.dist-info → jararaca-0.3.11a5.dist-info}/entry_points.txt +0 -0
|
@@ -2,9 +2,9 @@ from contextlib import asynccontextmanager
|
|
|
2
2
|
from typing import AsyncContextManager, AsyncGenerator, Protocol
|
|
3
3
|
|
|
4
4
|
from jararaca.microservice import (
|
|
5
|
-
AppContext,
|
|
6
5
|
AppInterceptor,
|
|
7
6
|
AppInterceptorWithLifecycle,
|
|
7
|
+
AppTransactionContext,
|
|
8
8
|
Container,
|
|
9
9
|
Microservice,
|
|
10
10
|
)
|
|
@@ -32,7 +32,9 @@ class ObservabilityInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
32
32
|
self.observability_provider = observability_provider
|
|
33
33
|
|
|
34
34
|
@asynccontextmanager
|
|
35
|
-
async def intercept(
|
|
35
|
+
async def intercept(
|
|
36
|
+
self, app_context: AppTransactionContext
|
|
37
|
+
) -> AsyncGenerator[None, None]:
|
|
36
38
|
|
|
37
39
|
async with self.observability_provider.tracing_provider.root_setup(app_context):
|
|
38
40
|
|
|
@@ -23,7 +23,7 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
23
23
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
24
24
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
25
25
|
|
|
26
|
-
from jararaca.microservice import
|
|
26
|
+
from jararaca.microservice import AppTransactionContext, Container, Microservice
|
|
27
27
|
from jararaca.observability.decorators import (
|
|
28
28
|
TracingContextProvider,
|
|
29
29
|
TracingContextProviderFactory,
|
|
@@ -36,7 +36,7 @@ tracer: trace.Tracer = trace.get_tracer(__name__)
|
|
|
36
36
|
|
|
37
37
|
class OtelTracingContextProvider(TracingContextProvider):
|
|
38
38
|
|
|
39
|
-
def __init__(self, app_context:
|
|
39
|
+
def __init__(self, app_context: AppTransactionContext) -> None:
|
|
40
40
|
self.app_context = app_context
|
|
41
41
|
|
|
42
42
|
@contextmanager
|
|
@@ -52,22 +52,26 @@ class OtelTracingContextProvider(TracingContextProvider):
|
|
|
52
52
|
|
|
53
53
|
class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
54
54
|
|
|
55
|
-
def provide_provider(
|
|
55
|
+
def provide_provider(
|
|
56
|
+
self, app_context: AppTransactionContext
|
|
57
|
+
) -> TracingContextProvider:
|
|
56
58
|
return OtelTracingContextProvider(app_context)
|
|
57
59
|
|
|
58
60
|
@asynccontextmanager
|
|
59
|
-
async def root_setup(
|
|
61
|
+
async def root_setup(
|
|
62
|
+
self, app_tx_ctx: AppTransactionContext
|
|
63
|
+
) -> AsyncGenerator[None, None]:
|
|
60
64
|
|
|
61
65
|
title: str = "Unmapped App Context Execution"
|
|
62
66
|
headers = {}
|
|
67
|
+
tx_data = app_tx_ctx.transaction_data
|
|
68
|
+
if tx_data.context_type == "http":
|
|
63
69
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
headers = dict(app_context.request.headers)
|
|
67
|
-
title = f"HTTP {app_context.request.method} {app_context.request.url}"
|
|
70
|
+
headers = dict(tx_data.request.headers)
|
|
71
|
+
title = f"HTTP {tx_data.request.method} {tx_data.request.url}"
|
|
68
72
|
|
|
69
|
-
elif
|
|
70
|
-
title = f"Message Bus {
|
|
73
|
+
elif tx_data.context_type == "message_bus":
|
|
74
|
+
title = f"Message Bus {tx_data.topic}"
|
|
71
75
|
|
|
72
76
|
carrier = {
|
|
73
77
|
key: value
|
|
@@ -11,15 +11,18 @@ from sqlalchemy.ext.asyncio import (
|
|
|
11
11
|
)
|
|
12
12
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
13
13
|
|
|
14
|
-
from jararaca.microservice import
|
|
14
|
+
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
15
|
+
from jararaca.reflect.metadata import SetMetadata, get_metadata_value
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
DEFAULT_CONNECTION_NAME = "default"
|
|
18
|
+
|
|
19
|
+
ctx_default_connection_name: ContextVar[str] = ContextVar(
|
|
20
|
+
"ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
|
|
21
|
+
)
|
|
17
22
|
|
|
18
23
|
|
|
19
24
|
def ensure_name(name: str | None) -> str:
|
|
20
|
-
|
|
21
|
-
return ctx_default_connection_name.get()
|
|
22
|
-
return name
|
|
25
|
+
return ctx_default_connection_name.get()
|
|
23
26
|
|
|
24
27
|
|
|
25
28
|
@dataclass
|
|
@@ -37,7 +40,10 @@ def providing_session(
|
|
|
37
40
|
tx: AsyncSessionTransaction,
|
|
38
41
|
connection_name: str | None = None,
|
|
39
42
|
) -> Generator[None, Any, None]:
|
|
40
|
-
|
|
43
|
+
"""
|
|
44
|
+
Context manager to provide a session and transaction for a specific connection name.
|
|
45
|
+
If no connection name is provided, it uses the default connection name from the context variable.
|
|
46
|
+
"""
|
|
41
47
|
connection_name = ensure_name(connection_name)
|
|
42
48
|
current_map = ctx_session_map.get({})
|
|
43
49
|
|
|
@@ -52,6 +58,12 @@ def providing_session(
|
|
|
52
58
|
ctx_session_map.reset(token)
|
|
53
59
|
|
|
54
60
|
|
|
61
|
+
provide_session = providing_session
|
|
62
|
+
"""
|
|
63
|
+
Alias for `providing_session` to maintain backward compatibility.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
55
67
|
@asynccontextmanager
|
|
56
68
|
async def providing_new_session(
|
|
57
69
|
connection_name: str | None = None,
|
|
@@ -70,7 +82,9 @@ def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
|
70
82
|
connection_name = ensure_name(connection_name)
|
|
71
83
|
current_map = ctx_session_map.get({})
|
|
72
84
|
if connection_name not in current_map:
|
|
73
|
-
raise ValueError(
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f'Session not found for connection "{connection_name}" in context. Check if your interceptor is correctly set up.'
|
|
87
|
+
)
|
|
74
88
|
|
|
75
89
|
return current_map[connection_name].session
|
|
76
90
|
|
|
@@ -102,10 +116,60 @@ def use_transaction(connection_name: str | None = None) -> AsyncSessionTransacti
|
|
|
102
116
|
class AIOSQAConfig:
|
|
103
117
|
url: str | AsyncEngine
|
|
104
118
|
connection_name: str
|
|
105
|
-
|
|
106
|
-
|
|
119
|
+
inject_default: bool
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
url: str | AsyncEngine,
|
|
124
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
125
|
+
inject_default: bool = True,
|
|
126
|
+
):
|
|
107
127
|
self.url = url
|
|
108
128
|
self.connection_name = connection_name
|
|
129
|
+
self.inject_default = inject_default
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
INJECT_CONNECTION_METADATA = "inject_connection_metadata_{connection_name}"
|
|
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
|
+
)
|
|
109
173
|
|
|
110
174
|
|
|
111
175
|
class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
@@ -121,7 +185,20 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
121
185
|
self.sessionmaker = async_sessionmaker(self.engine)
|
|
122
186
|
|
|
123
187
|
@asynccontextmanager
|
|
124
|
-
async def intercept(
|
|
188
|
+
async def intercept(
|
|
189
|
+
self, app_context: AppTransactionContext
|
|
190
|
+
) -> AsyncGenerator[None, None]:
|
|
191
|
+
|
|
192
|
+
uses_connection_metadata = get_metadata_value(
|
|
193
|
+
INJECT_CONNECTION_METADATA.format(
|
|
194
|
+
connection_name=self.config.connection_name
|
|
195
|
+
),
|
|
196
|
+
self.config.inject_default,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if not uses_connection_metadata:
|
|
200
|
+
yield
|
|
201
|
+
return
|
|
125
202
|
|
|
126
203
|
async with self.sessionmaker() as session, session.begin() as tx:
|
|
127
204
|
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
@@ -134,4 +211,5 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
134
211
|
await tx.rollback()
|
|
135
212
|
raise e
|
|
136
213
|
finally:
|
|
137
|
-
|
|
214
|
+
with suppress(ValueError):
|
|
215
|
+
ctx_default_connection_name.reset(token)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
1
|
+
from contextlib import contextmanager
|
|
2
|
+
from contextvars import ContextVar
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any, Awaitable, Callable, Literal, Protocol, TypeVar, cast
|
|
3
5
|
|
|
4
6
|
from fastapi import APIRouter
|
|
5
7
|
from fastapi import Depends as DependsF
|
|
@@ -9,6 +11,10 @@ from fastapi.params import Depends
|
|
|
9
11
|
from jararaca.lifecycle import AppLifecycle
|
|
10
12
|
from jararaca.presentation.http_microservice import HttpMiddleware
|
|
11
13
|
from jararaca.presentation.websocket.decorators import WebSocketEndpoint
|
|
14
|
+
from jararaca.reflect.controller_inspect import (
|
|
15
|
+
ControllerMemberReflect,
|
|
16
|
+
inspect_controller,
|
|
17
|
+
)
|
|
12
18
|
|
|
13
19
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
|
|
14
20
|
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
@@ -73,15 +79,15 @@ class RestController:
|
|
|
73
79
|
**(self.options or {}),
|
|
74
80
|
)
|
|
75
81
|
|
|
76
|
-
members =
|
|
82
|
+
controller, members = inspect_controller(cls)
|
|
77
83
|
|
|
78
84
|
router_members = [
|
|
79
|
-
(name, mapping)
|
|
80
|
-
for name, member in members
|
|
85
|
+
(name, mapping, member)
|
|
86
|
+
for name, member in members.items()
|
|
81
87
|
if (
|
|
82
88
|
mapping := (
|
|
83
|
-
HttpMapping.get_http_mapping(member)
|
|
84
|
-
or WebSocketEndpoint.get(member)
|
|
89
|
+
HttpMapping.get_http_mapping(member.member_function)
|
|
90
|
+
or WebSocketEndpoint.get(member.member_function)
|
|
85
91
|
)
|
|
86
92
|
)
|
|
87
93
|
is not None
|
|
@@ -89,7 +95,7 @@ class RestController:
|
|
|
89
95
|
|
|
90
96
|
router_members.sort(key=lambda x: x[1].order)
|
|
91
97
|
|
|
92
|
-
for name, mapping in router_members:
|
|
98
|
+
for name, mapping, member in router_members:
|
|
93
99
|
route_dependencies: list[Depends] = []
|
|
94
100
|
for middlewares_by_hook in UseMiddleware.get_middlewares(
|
|
95
101
|
getattr(instance, name)
|
|
@@ -104,12 +110,18 @@ class RestController:
|
|
|
104
110
|
):
|
|
105
111
|
route_dependencies.append(DependsF(dependency.dependency))
|
|
106
112
|
|
|
113
|
+
instance_method = getattr(instance, name)
|
|
114
|
+
instance_method = wraps_with_attributes(
|
|
115
|
+
instance_method,
|
|
116
|
+
controller_member_reflect=member,
|
|
117
|
+
)
|
|
118
|
+
|
|
107
119
|
if isinstance(mapping, HttpMapping):
|
|
108
120
|
try:
|
|
109
121
|
router.add_api_route(
|
|
110
122
|
methods=[mapping.method],
|
|
111
123
|
path=mapping.path,
|
|
112
|
-
endpoint=
|
|
124
|
+
endpoint=instance_method,
|
|
113
125
|
dependencies=route_dependencies,
|
|
114
126
|
**(mapping.options or {}),
|
|
115
127
|
)
|
|
@@ -120,7 +132,7 @@ class RestController:
|
|
|
120
132
|
else:
|
|
121
133
|
router.add_api_websocket_route(
|
|
122
134
|
path=mapping.path,
|
|
123
|
-
endpoint=
|
|
135
|
+
endpoint=instance_method,
|
|
124
136
|
dependencies=route_dependencies,
|
|
125
137
|
**(mapping.options or {}),
|
|
126
138
|
)
|
|
@@ -299,3 +311,77 @@ class UseDependency:
|
|
|
299
311
|
@staticmethod
|
|
300
312
|
def get_dependencies(subject: DECORATED_FUNC) -> list["UseDependency"]:
|
|
301
313
|
return getattr(subject, UseDependency.__DEPENDENCY_ATTR__, [])
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def wraps_with_member_data(
|
|
317
|
+
controller_member: ControllerMemberReflect, func: Callable[..., Awaitable[Any]]
|
|
318
|
+
) -> Callable[..., Any]:
|
|
319
|
+
"""
|
|
320
|
+
A decorator that wraps a function and preserves its metadata.
|
|
321
|
+
This is useful for preserving metadata when using decorators.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
@wraps(func)
|
|
325
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
326
|
+
|
|
327
|
+
with providing_controller_member(
|
|
328
|
+
controller_member=controller_member,
|
|
329
|
+
):
|
|
330
|
+
|
|
331
|
+
return await func(*args, **kwargs)
|
|
332
|
+
|
|
333
|
+
# Copy metadata from the original function to the wrapper
|
|
334
|
+
# for attr in dir(func):
|
|
335
|
+
# if not attr.startswith("__"):
|
|
336
|
+
# setattr(wrapper, attr, getattr(func, attr))
|
|
337
|
+
|
|
338
|
+
return wrapper
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
controller_member_ctxvar = ContextVar[ControllerMemberReflect](
|
|
342
|
+
"controller_member_ctxvar"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@contextmanager
|
|
347
|
+
def providing_controller_member(
|
|
348
|
+
controller_member: ControllerMemberReflect,
|
|
349
|
+
) -> Any:
|
|
350
|
+
"""
|
|
351
|
+
Context manager to provide the controller member metadata.
|
|
352
|
+
This is used to preserve the metadata of the controller member
|
|
353
|
+
when using decorators.
|
|
354
|
+
"""
|
|
355
|
+
token = controller_member_ctxvar.set(controller_member)
|
|
356
|
+
try:
|
|
357
|
+
yield
|
|
358
|
+
finally:
|
|
359
|
+
controller_member_ctxvar.reset(token)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def use_controller_member() -> ControllerMemberReflect:
|
|
363
|
+
"""
|
|
364
|
+
Get the current controller member metadata.
|
|
365
|
+
This is used to access the metadata of the controller member
|
|
366
|
+
when using decorators.
|
|
367
|
+
"""
|
|
368
|
+
return controller_member_ctxvar.get()
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def wraps_with_attributes(
|
|
372
|
+
func: Callable[..., Awaitable[Any]], **attributes: Any
|
|
373
|
+
) -> Callable[..., Awaitable[Any]]:
|
|
374
|
+
"""
|
|
375
|
+
A decorator that wraps a function and preserves its attributes.
|
|
376
|
+
This is useful for preserving attributes when using decorators.
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
@wraps(func)
|
|
380
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
381
|
+
return await func(*args, **kwargs)
|
|
382
|
+
|
|
383
|
+
# Copy attributes from the original function to the wrapper
|
|
384
|
+
for key, value in attributes.items():
|
|
385
|
+
setattr(wrapper, key, value)
|
|
386
|
+
|
|
387
|
+
return wrapper
|
jararaca/presentation/server.py
CHANGED
|
@@ -7,9 +7,14 @@ from starlette.types import ASGIApp
|
|
|
7
7
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
8
8
|
from jararaca.di import Container
|
|
9
9
|
from jararaca.lifecycle import AppLifecycle
|
|
10
|
-
from jararaca.microservice import
|
|
10
|
+
from jararaca.microservice import (
|
|
11
|
+
AppTransactionContext,
|
|
12
|
+
HttpTransactionData,
|
|
13
|
+
WebSocketTransactionData,
|
|
14
|
+
)
|
|
11
15
|
from jararaca.presentation.decorators import RestController
|
|
12
16
|
from jararaca.presentation.http_microservice import HttpMicroservice
|
|
17
|
+
from jararaca.reflect.controller_inspect import ControllerMemberReflect
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class HttpAppLifecycle:
|
|
@@ -79,10 +84,32 @@ class HttpUowContextProviderDependency:
|
|
|
79
84
|
async def __call__(
|
|
80
85
|
self, websocket: WebSocket = None, request: Request = None # type: ignore
|
|
81
86
|
) -> AsyncGenerator[None, None]:
|
|
87
|
+
if request:
|
|
88
|
+
endpoint = request.scope["endpoint"]
|
|
89
|
+
elif websocket:
|
|
90
|
+
endpoint = websocket.scope["endpoint"]
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError("Either request or websocket must be provided.")
|
|
93
|
+
|
|
94
|
+
member = getattr(endpoint, "controller_member_reflect", None)
|
|
95
|
+
|
|
96
|
+
if member is None:
|
|
97
|
+
raise ValueError("The endpoint does not have a controller member reflect.")
|
|
98
|
+
|
|
99
|
+
assert isinstance(member, ControllerMemberReflect), (
|
|
100
|
+
"Expected endpoint.controller_member_reflect to be of type "
|
|
101
|
+
"ControllerMemberReflect, but got: {}".format(type(member))
|
|
102
|
+
)
|
|
103
|
+
|
|
82
104
|
async with self.uow_provider(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
105
|
+
AppTransactionContext(
|
|
106
|
+
controller_member_reflect=member,
|
|
107
|
+
transaction_data=(
|
|
108
|
+
HttpTransactionData(request=request)
|
|
109
|
+
if request
|
|
110
|
+
else WebSocketTransactionData(websocket=websocket)
|
|
111
|
+
),
|
|
112
|
+
)
|
|
86
113
|
):
|
|
87
114
|
yield
|
|
88
115
|
|
|
@@ -13,9 +13,9 @@ from pydantic import BaseModel
|
|
|
13
13
|
from jararaca.core.uow import UnitOfWorkContextProvider
|
|
14
14
|
from jararaca.di import Container
|
|
15
15
|
from jararaca.microservice import (
|
|
16
|
-
AppContext,
|
|
17
16
|
AppInterceptor,
|
|
18
17
|
AppInterceptorWithLifecycle,
|
|
18
|
+
AppTransactionContext,
|
|
19
19
|
Microservice,
|
|
20
20
|
)
|
|
21
21
|
from jararaca.presentation.decorators import (
|
|
@@ -185,7 +185,9 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
|
|
|
185
185
|
self.shutdown_event.set()
|
|
186
186
|
|
|
187
187
|
@asynccontextmanager
|
|
188
|
-
async def intercept(
|
|
188
|
+
async def intercept(
|
|
189
|
+
self, app_context: AppTransactionContext
|
|
190
|
+
) -> AsyncGenerator[None, None]:
|
|
189
191
|
|
|
190
192
|
staging_ws_messages_sender = StagingWebSocketMessageSender(
|
|
191
193
|
self.connection_manager
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Callable, Mapping, Tuple, Type
|
|
4
|
+
|
|
5
|
+
from frozendict import frozendict
|
|
6
|
+
|
|
7
|
+
from jararaca.reflect.metadata import ControllerInstanceMetadata, SetMetadata
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class ControllerReflect:
|
|
12
|
+
|
|
13
|
+
controller_class: Type[Any]
|
|
14
|
+
metadata: Mapping[str, ControllerInstanceMetadata]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class ControllerMemberReflect:
|
|
19
|
+
controller_reflect: ControllerReflect
|
|
20
|
+
member_function: Callable[..., Any]
|
|
21
|
+
metadata: Mapping[str, ControllerInstanceMetadata]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def inspect_controller(
|
|
25
|
+
controller: Type[Any],
|
|
26
|
+
) -> Tuple[ControllerReflect, Mapping[str, ControllerMemberReflect]]:
|
|
27
|
+
"""
|
|
28
|
+
Inspect a controller class to extract its metadata and member functions.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
controller (Type[Any]): The controller class to inspect.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Tuple[ControllerReflect, list[ControllerMemberReflect]]: A tuple containing the controller reflect and a list of member reflects.
|
|
35
|
+
"""
|
|
36
|
+
controller_metadata_list = SetMetadata.get(controller)
|
|
37
|
+
|
|
38
|
+
controller_metadata_map = frozendict(
|
|
39
|
+
{
|
|
40
|
+
metadata.key: ControllerInstanceMetadata(
|
|
41
|
+
value=metadata.value, inherited=False
|
|
42
|
+
)
|
|
43
|
+
for metadata in controller_metadata_list
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
controller_reflect = ControllerReflect(
|
|
48
|
+
controller_class=controller, metadata=controller_metadata_map
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
members = {
|
|
52
|
+
name: ControllerMemberReflect(
|
|
53
|
+
controller_reflect=controller_reflect,
|
|
54
|
+
member_function=member,
|
|
55
|
+
metadata=frozendict(
|
|
56
|
+
{
|
|
57
|
+
**{
|
|
58
|
+
key: ControllerInstanceMetadata(
|
|
59
|
+
value=value.value, inherited=True
|
|
60
|
+
)
|
|
61
|
+
for key, value in controller_metadata_map.items()
|
|
62
|
+
},
|
|
63
|
+
**{
|
|
64
|
+
metadata.key: ControllerInstanceMetadata(
|
|
65
|
+
value=metadata.value, inherited=False
|
|
66
|
+
)
|
|
67
|
+
for metadata in SetMetadata.get(member)
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
for name, member in inspect.getmembers(controller, predicate=inspect.isfunction)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return controller_reflect, members
|
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
from contextlib import contextmanager, suppress
|
|
2
2
|
from contextvars import ContextVar
|
|
3
|
-
from
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
|
|
4
5
|
|
|
5
6
|
DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
|
|
6
7
|
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
@dataclass
|
|
10
|
+
class ControllerInstanceMetadata:
|
|
11
|
+
value: Any
|
|
12
|
+
inherited: bool
|
|
9
13
|
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
metadata_context: ContextVar[Mapping[str, ControllerInstanceMetadata]] = ContextVar(
|
|
16
|
+
"metadata_context"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_metadata(key: str) -> ControllerInstanceMetadata | None:
|
|
12
21
|
return metadata_context.get({}).get(key)
|
|
13
22
|
|
|
14
23
|
|
|
24
|
+
def get_metadata_value(key: str, default: Any | None = None) -> Any:
|
|
25
|
+
metadata = get_metadata(key)
|
|
26
|
+
if metadata is None:
|
|
27
|
+
return default
|
|
28
|
+
return metadata.value
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_all_metadata() -> Mapping[str, ControllerInstanceMetadata]:
|
|
32
|
+
return metadata_context.get({})
|
|
33
|
+
|
|
34
|
+
|
|
15
35
|
@contextmanager
|
|
16
|
-
def provide_metadata(metadata:
|
|
36
|
+
def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
|
|
17
37
|
|
|
18
38
|
current_metadata = metadata_context.get({})
|
|
19
39
|
|
|
@@ -39,7 +59,7 @@ class SetMetadata:
|
|
|
39
59
|
setattr(cls, SetMetadata.METATADA_LIST, metadata_list)
|
|
40
60
|
|
|
41
61
|
@staticmethod
|
|
42
|
-
def get(cls:
|
|
62
|
+
def get(cls: DECORATED) -> "list[SetMetadata]":
|
|
43
63
|
return cast(list[SetMetadata], getattr(cls, SetMetadata.METATADA_LIST, []))
|
|
44
64
|
|
|
45
65
|
def __call__(self, cls: DECORATED) -> DECORATED:
|
jararaca/scheduler/decorators.py
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
from
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Any, Awaitable, Callable, TypeVar, cast
|
|
4
|
+
|
|
5
|
+
from jararaca.reflect.controller_inspect import (
|
|
6
|
+
ControllerMemberReflect,
|
|
7
|
+
inspect_controller,
|
|
8
|
+
)
|
|
3
9
|
|
|
4
10
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
|
|
5
11
|
|
|
@@ -66,25 +72,6 @@ class ScheduledAction:
|
|
|
66
72
|
ScheduledAction, getattr(func, ScheduledAction.SCHEDULED_ACTION_ATTR)
|
|
67
73
|
)
|
|
68
74
|
|
|
69
|
-
@staticmethod
|
|
70
|
-
def get_type_scheduled_actions(
|
|
71
|
-
instance: Any,
|
|
72
|
-
) -> list[tuple[Callable[..., Any], "ScheduledAction"]]:
|
|
73
|
-
|
|
74
|
-
members = inspect.getmembers(instance, predicate=inspect.ismethod)
|
|
75
|
-
|
|
76
|
-
scheduled_actions: list[tuple[Callable[..., Any], "ScheduledAction"]] = []
|
|
77
|
-
|
|
78
|
-
for _, member in members:
|
|
79
|
-
scheduled_action = ScheduledAction.get_scheduled_action(member)
|
|
80
|
-
|
|
81
|
-
if scheduled_action is None:
|
|
82
|
-
continue
|
|
83
|
-
|
|
84
|
-
scheduled_actions.append((member, scheduled_action))
|
|
85
|
-
|
|
86
|
-
return scheduled_actions
|
|
87
|
-
|
|
88
75
|
@staticmethod
|
|
89
76
|
def get_function_id(
|
|
90
77
|
func: Callable[..., Any],
|
|
@@ -94,3 +81,44 @@ class ScheduledAction:
|
|
|
94
81
|
This is used to identify the scheduled action in the message broker.
|
|
95
82
|
"""
|
|
96
83
|
return f"{func.__module__}.{func.__qualname__}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class ScheduledActionData:
|
|
88
|
+
spec: ScheduledAction
|
|
89
|
+
controller_member: ControllerMemberReflect
|
|
90
|
+
callable: Callable[..., Awaitable[None]]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_type_scheduled_actions(
|
|
94
|
+
instance: Any,
|
|
95
|
+
) -> list[ScheduledActionData]:
|
|
96
|
+
|
|
97
|
+
_, member_metadata_map = inspect_controller(instance.__class__)
|
|
98
|
+
|
|
99
|
+
members = inspect.getmembers(instance, predicate=inspect.ismethod)
|
|
100
|
+
|
|
101
|
+
scheduled_actions: list[ScheduledActionData] = []
|
|
102
|
+
|
|
103
|
+
for name, member in members:
|
|
104
|
+
scheduled_action = ScheduledAction.get_scheduled_action(member)
|
|
105
|
+
|
|
106
|
+
if scheduled_action is None:
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
if name not in member_metadata_map:
|
|
110
|
+
raise Exception(
|
|
111
|
+
f"Member '{name}' is not a valid controller member in '{instance.__class__.__name__}'"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
member_metadata = member_metadata_map[name]
|
|
115
|
+
|
|
116
|
+
scheduled_actions.append(
|
|
117
|
+
ScheduledActionData(
|
|
118
|
+
callable=member,
|
|
119
|
+
spec=scheduled_action,
|
|
120
|
+
controller_member=member_metadata,
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return scheduled_actions
|