jararaca 0.3.11a3__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 +73 -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/base.py +2 -1
- jararaca/persistence/interceptors/aiosqa_interceptor.py +167 -16
- 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.11a3.dist-info → jararaca-0.3.11a5.dist-info}/METADATA +2 -1
- {jararaca-0.3.11a3.dist-info → jararaca-0.3.11a5.dist-info}/RECORD +28 -26
- {jararaca-0.3.11a3.dist-info → jararaca-0.3.11a5.dist-info}/LICENSE +0 -0
- {jararaca-0.3.11a3.dist-info → jararaca-0.3.11a5.dist-info}/WHEEL +0 -0
- {jararaca-0.3.11a3.dist-info → jararaca-0.3.11a5.dist-info}/entry_points.txt +0 -0
jararaca/microservice.py
CHANGED
|
@@ -23,64 +23,103 @@ from fastapi import Request, WebSocket
|
|
|
23
23
|
from jararaca.core.providers import ProviderSpec, T, Token
|
|
24
24
|
from jararaca.messagebus import MessageOf
|
|
25
25
|
from jararaca.messagebus.message import Message
|
|
26
|
+
from jararaca.reflect.controller_inspect import ControllerMemberReflect
|
|
26
27
|
|
|
27
28
|
if TYPE_CHECKING:
|
|
28
29
|
from typing_extensions import TypeIs
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
@dataclass
|
|
32
|
-
class
|
|
33
|
+
class SchedulerTransactionData:
|
|
33
34
|
triggered_at: datetime
|
|
34
35
|
scheduled_to: datetime
|
|
35
36
|
cron_expression: str
|
|
36
|
-
action: Callable[..., Any]
|
|
37
37
|
context_type: Literal["scheduler"] = "scheduler"
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
@dataclass
|
|
41
|
-
class
|
|
41
|
+
class HttpTransactionData:
|
|
42
42
|
request: Request
|
|
43
43
|
context_type: Literal["http"] = "http"
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
@dataclass
|
|
47
|
-
class
|
|
47
|
+
class MessageBusTransactionData:
|
|
48
48
|
topic: str
|
|
49
49
|
message: MessageOf[Message]
|
|
50
50
|
context_type: Literal["message_bus"] = "message_bus"
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
@dataclass
|
|
54
|
-
class
|
|
54
|
+
class WebSocketTransactionData:
|
|
55
55
|
websocket: WebSocket
|
|
56
56
|
context_type: Literal["websocket"] = "websocket"
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
TransactionData = (
|
|
60
|
+
MessageBusTransactionData
|
|
61
|
+
| HttpTransactionData
|
|
62
|
+
| SchedulerTransactionData
|
|
63
|
+
| WebSocketTransactionData
|
|
61
64
|
)
|
|
62
65
|
|
|
63
|
-
app_context_ctxvar = ContextVar[AppContext]("app_context")
|
|
64
66
|
|
|
67
|
+
@dataclass
|
|
68
|
+
class AppTransactionContext:
|
|
69
|
+
transaction_data: TransactionData
|
|
70
|
+
controller_member_reflect: ControllerMemberReflect
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
AppContext = AppTransactionContext
|
|
74
|
+
"""
|
|
75
|
+
Alias for AppTransactionContext, used for compatibility with existing code.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
app_transaction_context_var = ContextVar[AppTransactionContext]("app_context")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def use_app_transaction_context() -> AppTransactionContext:
|
|
83
|
+
"""
|
|
84
|
+
Returns the current application transaction context.
|
|
85
|
+
This function is used to access the application transaction context in the context of an application transaction.
|
|
86
|
+
If no context is set, it raises a LookupError.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
return app_transaction_context_var.get()
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
|
|
92
|
+
def use_app_tx_ctx_data() -> TransactionData:
|
|
93
|
+
"""
|
|
94
|
+
Returns the transaction data from the current app transaction context.
|
|
95
|
+
This function is used to access the transaction data in the context of an application transaction.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
return use_app_transaction_context().transaction_data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
use_app_context = use_app_tx_ctx_data
|
|
102
|
+
"""Alias for use_app_tx_ctx_data, used for compatibility with existing code."""
|
|
68
103
|
|
|
69
104
|
|
|
70
105
|
@contextmanager
|
|
71
|
-
def provide_app_context(
|
|
72
|
-
|
|
106
|
+
def provide_app_context(
|
|
107
|
+
app_context: AppTransactionContext,
|
|
108
|
+
) -> Generator[None, None, None]:
|
|
109
|
+
token = app_transaction_context_var.set(app_context)
|
|
73
110
|
try:
|
|
74
111
|
yield
|
|
75
112
|
finally:
|
|
76
113
|
with suppress(ValueError):
|
|
77
|
-
|
|
114
|
+
app_transaction_context_var.reset(token)
|
|
78
115
|
|
|
79
116
|
|
|
80
117
|
@runtime_checkable
|
|
81
118
|
class AppInterceptor(Protocol):
|
|
82
119
|
|
|
83
|
-
def intercept(
|
|
120
|
+
def intercept(
|
|
121
|
+
self, app_context: AppTransactionContext
|
|
122
|
+
) -> AsyncContextManager[None]: ...
|
|
84
123
|
|
|
85
124
|
|
|
86
125
|
class AppInterceptorWithLifecycle(Protocol):
|
|
@@ -227,17 +266,21 @@ def provide_container(container: Container) -> Generator[None, None, None]:
|
|
|
227
266
|
|
|
228
267
|
|
|
229
268
|
__all__ = [
|
|
230
|
-
"
|
|
269
|
+
"AppTransactionContext",
|
|
231
270
|
"AppInterceptor",
|
|
232
271
|
"AppInterceptorWithLifecycle",
|
|
233
272
|
"Container",
|
|
234
273
|
"Microservice",
|
|
235
|
-
"
|
|
236
|
-
"
|
|
237
|
-
"
|
|
274
|
+
"SchedulerTransactionData",
|
|
275
|
+
"WebSocketTransactionData",
|
|
276
|
+
"app_transaction_context_var",
|
|
238
277
|
"current_container_ctx",
|
|
239
278
|
"provide_app_context",
|
|
240
279
|
"provide_container",
|
|
241
280
|
"use_app_context",
|
|
242
281
|
"use_current_container",
|
|
282
|
+
"HttpTransactionData",
|
|
283
|
+
"MessageBusTransactionData",
|
|
284
|
+
"is_interceptor_with_lifecycle",
|
|
285
|
+
"AppContext",
|
|
243
286
|
]
|
|
@@ -13,7 +13,7 @@ from typing import (
|
|
|
13
13
|
TypeVar,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
|
-
from jararaca.microservice import
|
|
16
|
+
from jararaca.microservice import AppTransactionContext
|
|
17
17
|
|
|
18
18
|
P = ParamSpec("P")
|
|
19
19
|
R = TypeVar("R")
|
|
@@ -28,9 +28,13 @@ class TracingContextProvider(Protocol):
|
|
|
28
28
|
|
|
29
29
|
class TracingContextProviderFactory(Protocol):
|
|
30
30
|
|
|
31
|
-
def root_setup(
|
|
31
|
+
def root_setup(
|
|
32
|
+
self, app_context: AppTransactionContext
|
|
33
|
+
) -> AsyncContextManager[None]: ...
|
|
32
34
|
|
|
33
|
-
def provide_provider(
|
|
35
|
+
def provide_provider(
|
|
36
|
+
self, app_context: AppTransactionContext
|
|
37
|
+
) -> TracingContextProvider: ...
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
tracing_ctx_provider_ctxv = ContextVar[TracingContextProvider]("tracing_ctx_provider")
|
|
@@ -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
|
jararaca/persistence/base.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from typing import Any, Self, Type, TypeVar
|
|
2
2
|
|
|
3
3
|
from pydantic import BaseModel
|
|
4
|
+
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
4
5
|
from sqlalchemy.orm import DeclarativeBase
|
|
5
6
|
|
|
6
7
|
IDENTIFIABLE_SCHEMA_T = TypeVar("IDENTIFIABLE_SCHEMA_T")
|
|
@@ -20,7 +21,7 @@ def recursive_get_dict(obj: Any) -> Any:
|
|
|
20
21
|
return obj
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class BaseEntity(DeclarativeBase):
|
|
24
|
+
class BaseEntity(AsyncAttrs, DeclarativeBase):
|
|
24
25
|
|
|
25
26
|
@classmethod
|
|
26
27
|
def from_basemodel(cls, mutation: T_BASEMODEL) -> "Self":
|
|
@@ -3,21 +3,53 @@ from contextvars import ContextVar
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from typing import Any, AsyncGenerator, Generator
|
|
5
5
|
|
|
6
|
-
from sqlalchemy.ext.asyncio import
|
|
6
|
+
from sqlalchemy.ext.asyncio import (
|
|
7
|
+
AsyncSession,
|
|
8
|
+
AsyncSessionTransaction,
|
|
9
|
+
async_sessionmaker,
|
|
10
|
+
create_async_engine,
|
|
11
|
+
)
|
|
7
12
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
8
13
|
|
|
9
|
-
from jararaca.microservice import
|
|
14
|
+
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
15
|
+
from jararaca.reflect.metadata import SetMetadata, get_metadata_value
|
|
10
16
|
|
|
11
|
-
|
|
17
|
+
DEFAULT_CONNECTION_NAME = "default"
|
|
18
|
+
|
|
19
|
+
ctx_default_connection_name: ContextVar[str] = ContextVar(
|
|
20
|
+
"ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def ensure_name(name: str | None) -> str:
|
|
25
|
+
return ctx_default_connection_name.get()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class PersistenceCtx:
|
|
30
|
+
session: AsyncSession
|
|
31
|
+
tx: AsyncSessionTransaction
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
ctx_session_map = ContextVar[dict[str, PersistenceCtx]]("ctx_session_map", default={})
|
|
12
35
|
|
|
13
36
|
|
|
14
37
|
@contextmanager
|
|
15
|
-
def
|
|
16
|
-
|
|
38
|
+
def providing_session(
|
|
39
|
+
session: AsyncSession,
|
|
40
|
+
tx: AsyncSessionTransaction,
|
|
41
|
+
connection_name: str | None = None,
|
|
17
42
|
) -> Generator[None, Any, None]:
|
|
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
|
+
"""
|
|
47
|
+
connection_name = ensure_name(connection_name)
|
|
18
48
|
current_map = ctx_session_map.get({})
|
|
19
49
|
|
|
20
|
-
token = ctx_session_map.set(
|
|
50
|
+
token = ctx_session_map.set(
|
|
51
|
+
{**current_map, connection_name: PersistenceCtx(session, tx)}
|
|
52
|
+
)
|
|
21
53
|
|
|
22
54
|
try:
|
|
23
55
|
yield
|
|
@@ -26,18 +58,118 @@ def provide_session(
|
|
|
26
58
|
ctx_session_map.reset(token)
|
|
27
59
|
|
|
28
60
|
|
|
29
|
-
|
|
61
|
+
provide_session = providing_session
|
|
62
|
+
"""
|
|
63
|
+
Alias for `providing_session` to maintain backward compatibility.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@asynccontextmanager
|
|
68
|
+
async def providing_new_session(
|
|
69
|
+
connection_name: str | None = None,
|
|
70
|
+
) -> AsyncGenerator[AsyncSession, None]:
|
|
71
|
+
|
|
72
|
+
current_session = use_session(connection_name)
|
|
73
|
+
|
|
74
|
+
async with AsyncSession(
|
|
75
|
+
current_session.bind,
|
|
76
|
+
) as new_session, new_session.begin() as new_tx:
|
|
77
|
+
with providing_session(new_session, new_tx, connection_name):
|
|
78
|
+
yield new_session
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
82
|
+
connection_name = ensure_name(connection_name)
|
|
30
83
|
current_map = ctx_session_map.get({})
|
|
31
84
|
if connection_name not in current_map:
|
|
32
|
-
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
|
+
)
|
|
33
88
|
|
|
34
|
-
return current_map[connection_name]
|
|
89
|
+
return current_map[connection_name].session
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@contextmanager
|
|
93
|
+
def providing_transaction(
|
|
94
|
+
tx: AsyncSessionTransaction,
|
|
95
|
+
connection_name: str | None = None,
|
|
96
|
+
) -> Generator[None, Any, None]:
|
|
97
|
+
connection_name = ensure_name(connection_name)
|
|
98
|
+
|
|
99
|
+
current_map = ctx_session_map.get({})
|
|
100
|
+
|
|
101
|
+
if connection_name not in current_map:
|
|
102
|
+
raise ValueError(f"No session found for connection {connection_name}")
|
|
103
|
+
|
|
104
|
+
with providing_session(current_map[connection_name].session, tx, connection_name):
|
|
105
|
+
yield
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def use_transaction(connection_name: str | None = None) -> AsyncSessionTransaction:
|
|
109
|
+
current_map = ctx_session_map.get({})
|
|
110
|
+
if connection_name not in current_map:
|
|
111
|
+
raise ValueError(f"Transaction not found for connection {connection_name}")
|
|
112
|
+
|
|
113
|
+
return current_map[connection_name].tx
|
|
35
114
|
|
|
36
115
|
|
|
37
|
-
@dataclass
|
|
38
116
|
class AIOSQAConfig:
|
|
39
|
-
connection_name: str
|
|
40
117
|
url: str | AsyncEngine
|
|
118
|
+
connection_name: str
|
|
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
|
+
):
|
|
127
|
+
self.url = url
|
|
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
|
+
)
|
|
41
173
|
|
|
42
174
|
|
|
43
175
|
class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
@@ -53,12 +185,31 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
53
185
|
self.sessionmaker = async_sessionmaker(self.engine)
|
|
54
186
|
|
|
55
187
|
@asynccontextmanager
|
|
56
|
-
async def intercept(
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
202
|
+
|
|
203
|
+
async with self.sessionmaker() as session, session.begin() as tx:
|
|
204
|
+
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
205
|
+
with providing_session(session, tx, self.config.connection_name):
|
|
59
206
|
try:
|
|
60
207
|
yield
|
|
61
|
-
|
|
208
|
+
if tx.is_active:
|
|
209
|
+
await tx.commit()
|
|
62
210
|
except Exception as e:
|
|
63
|
-
await
|
|
211
|
+
await tx.rollback()
|
|
64
212
|
raise e
|
|
213
|
+
finally:
|
|
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
|
|