jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a5__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.
- README.md +121 -0
- jararaca/__init__.py +184 -12
- jararaca/__main__.py +4 -0
- jararaca/broker_backend/__init__.py +4 -0
- jararaca/broker_backend/mapper.py +4 -0
- jararaca/broker_backend/redis_broker_backend.py +9 -3
- jararaca/cli.py +272 -47
- jararaca/common/__init__.py +3 -0
- jararaca/core/__init__.py +3 -0
- jararaca/core/providers.py +4 -0
- jararaca/core/uow.py +41 -7
- jararaca/di.py +4 -0
- jararaca/files/entity.py.mako +4 -0
- jararaca/lifecycle.py +6 -2
- jararaca/messagebus/__init__.py +4 -0
- jararaca/messagebus/bus_message_controller.py +4 -0
- jararaca/messagebus/consumers/__init__.py +3 -0
- jararaca/messagebus/decorators.py +33 -67
- jararaca/messagebus/implicit_headers.py +49 -0
- jararaca/messagebus/interceptors/__init__.py +3 -0
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
- jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
- jararaca/messagebus/message.py +4 -0
- jararaca/messagebus/publisher.py +6 -0
- jararaca/messagebus/worker.py +850 -383
- jararaca/microservice.py +110 -1
- jararaca/observability/constants.py +7 -0
- jararaca/observability/decorators.py +170 -13
- jararaca/observability/fastapi_exception_handler.py +37 -0
- jararaca/observability/hooks.py +109 -0
- jararaca/observability/interceptor.py +4 -0
- jararaca/observability/providers/__init__.py +3 -0
- jararaca/observability/providers/otel.py +202 -11
- jararaca/persistence/base.py +38 -2
- jararaca/persistence/exports.py +4 -0
- jararaca/persistence/interceptors/__init__.py +3 -0
- jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
- jararaca/persistence/interceptors/constants.py +5 -0
- jararaca/persistence/interceptors/decorators.py +50 -0
- jararaca/persistence/session.py +3 -0
- jararaca/persistence/sort_filter.py +4 -0
- jararaca/persistence/utilities.py +50 -20
- jararaca/presentation/__init__.py +3 -0
- jararaca/presentation/decorators.py +88 -86
- jararaca/presentation/exceptions.py +23 -0
- jararaca/presentation/hooks.py +4 -0
- jararaca/presentation/http_microservice.py +4 -0
- jararaca/presentation/server.py +97 -45
- jararaca/presentation/websocket/__init__.py +3 -0
- jararaca/presentation/websocket/base_types.py +4 -0
- jararaca/presentation/websocket/context.py +4 -0
- jararaca/presentation/websocket/decorators.py +8 -41
- jararaca/presentation/websocket/redis.py +280 -53
- jararaca/presentation/websocket/types.py +4 -0
- jararaca/presentation/websocket/websocket_interceptor.py +46 -19
- jararaca/reflect/__init__.py +3 -0
- jararaca/reflect/controller_inspect.py +16 -10
- jararaca/reflect/decorators.py +238 -0
- jararaca/reflect/metadata.py +34 -25
- jararaca/rpc/__init__.py +3 -0
- jararaca/rpc/http/__init__.py +101 -0
- jararaca/rpc/http/backends/__init__.py +14 -0
- jararaca/rpc/http/backends/httpx.py +43 -9
- jararaca/rpc/http/backends/otel.py +4 -0
- jararaca/rpc/http/decorators.py +378 -113
- jararaca/rpc/http/httpx.py +3 -0
- jararaca/scheduler/__init__.py +3 -0
- jararaca/scheduler/beat_worker.py +521 -105
- jararaca/scheduler/decorators.py +15 -22
- jararaca/scheduler/types.py +4 -0
- jararaca/tools/app_config/__init__.py +3 -0
- jararaca/tools/app_config/decorators.py +7 -19
- jararaca/tools/app_config/interceptor.py +6 -2
- jararaca/tools/typescript/__init__.py +3 -0
- jararaca/tools/typescript/decorators.py +120 -0
- jararaca/tools/typescript/interface_parser.py +1074 -173
- jararaca/utils/__init__.py +3 -0
- jararaca/utils/rabbitmq_utils.py +65 -39
- jararaca/utils/retry.py +10 -3
- jararaca-0.4.0a5.dist-info/LICENSE +674 -0
- jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
- jararaca-0.4.0a5.dist-info/RECORD +88 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
- pyproject.toml +131 -0
- jararaca-0.3.11a16.dist-info/RECORD +0 -74
- /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
- {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
import logging
|
|
2
6
|
from contextlib import asynccontextmanager, contextmanager
|
|
3
|
-
from typing import AsyncGenerator, Generator, Protocol
|
|
7
|
+
from typing import Any, AsyncGenerator, Generator, Literal, Protocol
|
|
4
8
|
|
|
5
9
|
from opentelemetry import metrics, trace
|
|
6
10
|
from opentelemetry._logs import set_logger_provider
|
|
@@ -23,32 +27,159 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
23
27
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
24
28
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
25
29
|
|
|
26
|
-
from jararaca.
|
|
30
|
+
from jararaca.messagebus.implicit_headers import (
|
|
31
|
+
ImplicitHeaders,
|
|
32
|
+
provide_implicit_headers,
|
|
33
|
+
use_implicit_headers,
|
|
34
|
+
)
|
|
35
|
+
from jararaca.microservice import (
|
|
36
|
+
AppTransactionContext,
|
|
37
|
+
Container,
|
|
38
|
+
Microservice,
|
|
39
|
+
use_app_transaction_context,
|
|
40
|
+
)
|
|
41
|
+
from jararaca.observability.constants import TRACEPARENT_KEY
|
|
27
42
|
from jararaca.observability.decorators import (
|
|
43
|
+
AttributeMap,
|
|
44
|
+
AttributeValue,
|
|
28
45
|
TracingContextProvider,
|
|
29
46
|
TracingContextProviderFactory,
|
|
30
|
-
|
|
47
|
+
TracingSpan,
|
|
48
|
+
TracingSpanContext,
|
|
31
49
|
)
|
|
32
50
|
from jararaca.observability.interceptor import ObservabilityProvider
|
|
33
51
|
|
|
34
52
|
tracer: trace.Tracer = trace.get_tracer(__name__)
|
|
35
53
|
|
|
36
54
|
|
|
55
|
+
def extract_context_attributes(ctx: AppTransactionContext) -> dict[str, Any]:
|
|
56
|
+
tx_data = ctx.transaction_data
|
|
57
|
+
extra_attributes: dict[str, Any] = {}
|
|
58
|
+
|
|
59
|
+
if tx_data.context_type == "http":
|
|
60
|
+
extra_attributes = {
|
|
61
|
+
"http.method": tx_data.request.method,
|
|
62
|
+
"http.url": str(tx_data.request.url),
|
|
63
|
+
"http.path": tx_data.request.url.path,
|
|
64
|
+
"http.route.path": tx_data.request.scope["route"].path,
|
|
65
|
+
"http.route.endpoint.name": tx_data.request["route"].endpoint.__qualname__,
|
|
66
|
+
"http.query": tx_data.request.url.query,
|
|
67
|
+
**{
|
|
68
|
+
f"http.request.path_param.{k}": v
|
|
69
|
+
for k, v in tx_data.request.path_params.items()
|
|
70
|
+
},
|
|
71
|
+
**{
|
|
72
|
+
f"http.request.query_param.{k}": v
|
|
73
|
+
for k, v in tx_data.request.query_params.items()
|
|
74
|
+
},
|
|
75
|
+
**{
|
|
76
|
+
f"http.request.header.{k}": v
|
|
77
|
+
for k, v in tx_data.request.headers.items()
|
|
78
|
+
},
|
|
79
|
+
"http.request.client.host": (
|
|
80
|
+
tx_data.request.client.host if tx_data.request.client else ""
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
elif tx_data.context_type == "message_bus":
|
|
84
|
+
extra_attributes = {
|
|
85
|
+
"bus.message.name": tx_data.message_type.__qualname__,
|
|
86
|
+
"bus.message.module": tx_data.message_type.__module__,
|
|
87
|
+
"bus.message.category": tx_data.message_type.MESSAGE_CATEGORY,
|
|
88
|
+
"bus.message.type": tx_data.message_type.MESSAGE_TYPE,
|
|
89
|
+
"bus.message.topic": tx_data.message_type.MESSAGE_TOPIC,
|
|
90
|
+
}
|
|
91
|
+
elif tx_data.context_type == "websocket":
|
|
92
|
+
extra_attributes = {
|
|
93
|
+
"ws.url": str(tx_data.websocket.url),
|
|
94
|
+
}
|
|
95
|
+
elif tx_data.context_type == "scheduler":
|
|
96
|
+
extra_attributes = {
|
|
97
|
+
"sched.task_name": tx_data.task_name,
|
|
98
|
+
"sched.scheduled_to": tx_data.scheduled_to.isoformat(),
|
|
99
|
+
"sched.cron_expression": tx_data.cron_expression,
|
|
100
|
+
"sched.triggered_at": tx_data.triggered_at.isoformat(),
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
"app.context_type": tx_data.context_type,
|
|
104
|
+
"controller_member_reflect.rest_controller.class_name": ctx.controller_member_reflect.controller_reflect.controller_class.__qualname__,
|
|
105
|
+
"controller_member_reflect.rest_controller.module": ctx.controller_member_reflect.controller_reflect.controller_class.__module__,
|
|
106
|
+
"controller_member_reflect.member_function.name": ctx.controller_member_reflect.member_function.__qualname__,
|
|
107
|
+
"controller_member_reflect.member_function.module": ctx.controller_member_reflect.member_function.__module__,
|
|
108
|
+
**extra_attributes,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class OtelTracingSpan(TracingSpan):
|
|
113
|
+
|
|
114
|
+
def __init__(self, span: trace.Span) -> None:
|
|
115
|
+
self.span = span
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class OtelTracingSpanContext(TracingSpanContext):
|
|
119
|
+
|
|
120
|
+
def __init__(self, span_context: trace.SpanContext) -> None:
|
|
121
|
+
self.span_context = span_context
|
|
122
|
+
|
|
123
|
+
|
|
37
124
|
class OtelTracingContextProvider(TracingContextProvider):
|
|
38
125
|
|
|
39
126
|
def __init__(self, app_context: AppTransactionContext) -> None:
|
|
40
127
|
self.app_context = app_context
|
|
41
128
|
|
|
42
129
|
@contextmanager
|
|
43
|
-
def
|
|
130
|
+
def start_span_context(
|
|
44
131
|
self,
|
|
45
132
|
trace_name: str,
|
|
46
|
-
context_attributes:
|
|
133
|
+
context_attributes: AttributeMap | None,
|
|
47
134
|
) -> Generator[None, None, None]:
|
|
48
135
|
|
|
49
136
|
with tracer.start_as_current_span(trace_name, attributes=context_attributes):
|
|
50
137
|
yield
|
|
51
138
|
|
|
139
|
+
def add_event(
|
|
140
|
+
self, event_name: str, event_attributes: AttributeMap | None = None
|
|
141
|
+
) -> None:
|
|
142
|
+
trace.get_current_span().add_event(name=event_name, attributes=event_attributes)
|
|
143
|
+
|
|
144
|
+
def set_span_status(self, status_code: Literal["OK", "ERROR", "UNSET"]) -> None:
|
|
145
|
+
span = trace.get_current_span()
|
|
146
|
+
if status_code == "OK":
|
|
147
|
+
span.set_status(trace.Status(trace.StatusCode.OK))
|
|
148
|
+
elif status_code == "ERROR":
|
|
149
|
+
span.set_status(trace.Status(trace.StatusCode.ERROR))
|
|
150
|
+
else:
|
|
151
|
+
span.set_status(trace.Status(trace.StatusCode.UNSET))
|
|
152
|
+
|
|
153
|
+
def record_exception(
|
|
154
|
+
self,
|
|
155
|
+
exception: Exception,
|
|
156
|
+
attributes: AttributeMap | None = None,
|
|
157
|
+
escaped: bool = False,
|
|
158
|
+
) -> None:
|
|
159
|
+
span = trace.get_current_span()
|
|
160
|
+
span.record_exception(exception, attributes=attributes, escaped=escaped)
|
|
161
|
+
|
|
162
|
+
def set_span_attribute(self, key: str, value: AttributeValue) -> None:
|
|
163
|
+
span = trace.get_current_span()
|
|
164
|
+
span.set_attribute(key, value)
|
|
165
|
+
|
|
166
|
+
def update_span_name(self, new_name: str) -> None:
|
|
167
|
+
span = trace.get_current_span()
|
|
168
|
+
|
|
169
|
+
span.update_name(new_name)
|
|
170
|
+
|
|
171
|
+
def add_link(self, span_context: TracingSpanContext) -> None:
|
|
172
|
+
if not isinstance(span_context, OtelTracingSpanContext):
|
|
173
|
+
return
|
|
174
|
+
span = trace.get_current_span()
|
|
175
|
+
span.add_link(span_context.span_context)
|
|
176
|
+
|
|
177
|
+
def get_current_span(self) -> TracingSpan | None:
|
|
178
|
+
return OtelTracingSpan(trace.get_current_span())
|
|
179
|
+
|
|
180
|
+
def get_current_span_context(self) -> TracingSpanContext | None:
|
|
181
|
+
return OtelTracingSpanContext(trace.get_current_span().get_span_context())
|
|
182
|
+
|
|
52
183
|
|
|
53
184
|
class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
54
185
|
|
|
@@ -63,15 +194,27 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
|
63
194
|
) -> AsyncGenerator[None, None]:
|
|
64
195
|
|
|
65
196
|
title: str = "Unmapped App Context Execution"
|
|
66
|
-
headers = {}
|
|
197
|
+
headers: dict[str, Any] = {}
|
|
67
198
|
tx_data = app_tx_ctx.transaction_data
|
|
68
|
-
|
|
199
|
+
extra_attributes = extract_context_attributes(app_tx_ctx)
|
|
69
200
|
|
|
201
|
+
if tx_data.context_type == "http":
|
|
70
202
|
headers = dict(tx_data.request.headers)
|
|
71
203
|
title = f"HTTP {tx_data.request.method} {tx_data.request.url}"
|
|
204
|
+
extra_attributes["http.request.body"] = (await tx_data.request.body())[
|
|
205
|
+
:5000
|
|
206
|
+
].decode(errors="ignore")
|
|
72
207
|
|
|
73
208
|
elif tx_data.context_type == "message_bus":
|
|
74
209
|
title = f"Message Bus {tx_data.topic}"
|
|
210
|
+
headers = use_implicit_headers() or {}
|
|
211
|
+
|
|
212
|
+
elif tx_data.context_type == "websocket":
|
|
213
|
+
headers = dict(tx_data.websocket.headers)
|
|
214
|
+
title = f"WebSocket {tx_data.websocket.url}"
|
|
215
|
+
|
|
216
|
+
elif tx_data.context_type == "scheduler":
|
|
217
|
+
title = f"Scheduler Task {tx_data.task_name}"
|
|
75
218
|
|
|
76
219
|
carrier = {
|
|
77
220
|
key: value
|
|
@@ -90,8 +233,28 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
|
90
233
|
|
|
91
234
|
ctx2 = W3CBaggagePropagator().extract(b2, context=ctx)
|
|
92
235
|
|
|
93
|
-
with tracer.start_as_current_span(
|
|
94
|
-
|
|
236
|
+
with tracer.start_as_current_span(
|
|
237
|
+
name=title,
|
|
238
|
+
context=ctx2,
|
|
239
|
+
attributes={
|
|
240
|
+
**extra_attributes,
|
|
241
|
+
},
|
|
242
|
+
) as root_span:
|
|
243
|
+
cx = root_span.get_span_context()
|
|
244
|
+
span_traceparent_id = hex(cx.trace_id)[2:].rjust(32, "0")
|
|
245
|
+
if app_tx_ctx.transaction_data.context_type == "http":
|
|
246
|
+
app_tx_ctx.transaction_data.request.scope[TRACEPARENT_KEY] = (
|
|
247
|
+
span_traceparent_id
|
|
248
|
+
)
|
|
249
|
+
elif app_tx_ctx.transaction_data.context_type == "websocket":
|
|
250
|
+
app_tx_ctx.transaction_data.websocket.scope[TRACEPARENT_KEY] = (
|
|
251
|
+
span_traceparent_id
|
|
252
|
+
)
|
|
253
|
+
tracing_headers: ImplicitHeaders = {}
|
|
254
|
+
TraceContextTextMapPropagator().inject(tracing_headers)
|
|
255
|
+
W3CBaggagePropagator().inject(tracing_headers)
|
|
256
|
+
with provide_implicit_headers(tracing_headers):
|
|
257
|
+
yield
|
|
95
258
|
|
|
96
259
|
|
|
97
260
|
class LoggerHandlerCallback(Protocol):
|
|
@@ -99,6 +262,34 @@ class LoggerHandlerCallback(Protocol):
|
|
|
99
262
|
def __call__(self, logger_handler: logging.Handler) -> None: ...
|
|
100
263
|
|
|
101
264
|
|
|
265
|
+
class CustomLoggingHandler(LoggingHandler):
|
|
266
|
+
|
|
267
|
+
def _translate(self, record: logging.LogRecord) -> dict[str, Any]:
|
|
268
|
+
try:
|
|
269
|
+
ctx = use_app_transaction_context()
|
|
270
|
+
data = super()._translate(record)
|
|
271
|
+
extra_attributes = extract_context_attributes(ctx)
|
|
272
|
+
|
|
273
|
+
current_span = trace.get_current_span()
|
|
274
|
+
|
|
275
|
+
data["attributes"] = {
|
|
276
|
+
**data.get("attributes", {}),
|
|
277
|
+
**extra_attributes,
|
|
278
|
+
**(
|
|
279
|
+
{
|
|
280
|
+
"span_name": current_span.name,
|
|
281
|
+
}
|
|
282
|
+
if hasattr(current_span, "name")
|
|
283
|
+
and current_span.is_recording() is False
|
|
284
|
+
else {}
|
|
285
|
+
),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return data
|
|
289
|
+
except LookupError:
|
|
290
|
+
return super()._translate(record)
|
|
291
|
+
|
|
292
|
+
|
|
102
293
|
class OtelObservabilityProvider(ObservabilityProvider):
|
|
103
294
|
|
|
104
295
|
def __init__(
|
|
@@ -143,11 +334,11 @@ class OtelObservabilityProvider(ObservabilityProvider):
|
|
|
143
334
|
BatchLogRecordProcessor(self.logs_exporter)
|
|
144
335
|
)
|
|
145
336
|
|
|
146
|
-
logging_handler =
|
|
337
|
+
logging_handler = CustomLoggingHandler(
|
|
147
338
|
level=logging.DEBUG, logger_provider=logger_provider
|
|
148
339
|
)
|
|
149
340
|
|
|
150
|
-
logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
|
|
341
|
+
# logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
|
|
151
342
|
|
|
152
343
|
self.logging_handler_callback(logging_handler)
|
|
153
344
|
|
jararaca/persistence/base.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
from typing import Any, Callable, Protocol, Self, Type, TypeVar
|
|
2
6
|
|
|
3
7
|
from pydantic import BaseModel
|
|
4
8
|
from sqlalchemy.ext.asyncio import AsyncAttrs
|
|
@@ -21,14 +25,46 @@ def recursive_get_dict(obj: Any) -> Any:
|
|
|
21
25
|
return obj
|
|
22
26
|
|
|
23
27
|
|
|
28
|
+
RESULT_T = TypeVar("RESULT_T", covariant=True)
|
|
29
|
+
ENTITY_T_CONTRA = TypeVar("ENTITY_T_CONTRA", bound="BaseEntity", contravariant=True)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class EntityParserType(Protocol[ENTITY_T_CONTRA, RESULT_T]):
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def parse_entity(cls, model: ENTITY_T_CONTRA) -> "RESULT_T": ...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
EntityParserFunc = Callable[[ENTITY_T_CONTRA], RESULT_T]
|
|
39
|
+
|
|
40
|
+
BASED_BASE_ENTITY_T = TypeVar("BASED_BASE_ENTITY_T", bound="BaseEntity")
|
|
41
|
+
|
|
42
|
+
|
|
24
43
|
class BaseEntity(AsyncAttrs, DeclarativeBase):
|
|
25
44
|
|
|
26
45
|
@classmethod
|
|
27
46
|
def from_basemodel(cls, mutation: T_BASEMODEL) -> "Self":
|
|
28
47
|
intersection = set(cls.__annotations__.keys()) & set(
|
|
29
|
-
mutation.model_fields.keys()
|
|
48
|
+
mutation.__class__.model_fields.keys()
|
|
30
49
|
)
|
|
31
50
|
return cls(**{k: getattr(mutation, k) for k in intersection})
|
|
32
51
|
|
|
33
52
|
def to_basemodel(self, model: Type[T_BASEMODEL]) -> T_BASEMODEL:
|
|
34
53
|
return model.model_validate(recursive_get_dict(self))
|
|
54
|
+
|
|
55
|
+
def parse_entity_with_func(
|
|
56
|
+
self, model_cls: EntityParserFunc["Self", RESULT_T]
|
|
57
|
+
) -> RESULT_T:
|
|
58
|
+
return model_cls(self)
|
|
59
|
+
|
|
60
|
+
def parse_entity_with_type(
|
|
61
|
+
self: BASED_BASE_ENTITY_T,
|
|
62
|
+
model_cls: Type[EntityParserType[BASED_BASE_ENTITY_T, RESULT_T]],
|
|
63
|
+
) -> RESULT_T:
|
|
64
|
+
return model_cls.parse_entity(self)
|
|
65
|
+
|
|
66
|
+
def __rshift__(self, model: EntityParserFunc["Self", RESULT_T]) -> RESULT_T:
|
|
67
|
+
return self.parse_entity_with_func(model)
|
|
68
|
+
|
|
69
|
+
def __and__(self, model: Type[EntityParserType["Self", RESULT_T]]) -> RESULT_T:
|
|
70
|
+
return self.parse_entity_with_type(model)
|
jararaca/persistence/exports.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
1
5
|
from contextlib import asynccontextmanager, contextmanager, suppress
|
|
2
6
|
from contextvars import ContextVar
|
|
3
7
|
from dataclasses import dataclass
|
|
4
|
-
from typing import Any, AsyncGenerator, Generator
|
|
8
|
+
from typing import Any, AsyncGenerator, Generator, Protocol
|
|
5
9
|
|
|
6
10
|
from sqlalchemy.ext.asyncio import (
|
|
7
11
|
AsyncSession,
|
|
@@ -12,9 +16,47 @@ from sqlalchemy.ext.asyncio import (
|
|
|
12
16
|
from sqlalchemy.ext.asyncio.engine import AsyncEngine
|
|
13
17
|
|
|
14
18
|
from jararaca.microservice import AppInterceptor, AppTransactionContext
|
|
15
|
-
from jararaca.
|
|
19
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
20
|
+
from jararaca.persistence.interceptors.decorators import (
|
|
21
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE,
|
|
22
|
+
)
|
|
23
|
+
from jararaca.reflect.metadata import get_metadata_value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SessionManager(Protocol):
|
|
27
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession: ...
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
ctx_session_manager: ContextVar[SessionManager | None] = ContextVar(
|
|
31
|
+
"ctx_session_manager", default=None
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@contextmanager
|
|
36
|
+
def providing_session_manager(
|
|
37
|
+
session_manager: SessionManager,
|
|
38
|
+
) -> Generator[None, Any, None]:
|
|
39
|
+
"""
|
|
40
|
+
Context manager to provide a session manager for the current context.
|
|
41
|
+
"""
|
|
42
|
+
token = ctx_session_manager.set(session_manager)
|
|
43
|
+
try:
|
|
44
|
+
yield
|
|
45
|
+
finally:
|
|
46
|
+
with suppress(ValueError):
|
|
47
|
+
ctx_session_manager.reset(token)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def use_session_manager() -> SessionManager:
|
|
51
|
+
"""
|
|
52
|
+
Retrieve the current session manager from the context variable.
|
|
53
|
+
Raises ValueError if no session manager is set.
|
|
54
|
+
"""
|
|
55
|
+
session_manager = ctx_session_manager.get()
|
|
56
|
+
if session_manager is None:
|
|
57
|
+
raise ValueError("No session manager set in the context.")
|
|
58
|
+
return session_manager
|
|
16
59
|
|
|
17
|
-
DEFAULT_CONNECTION_NAME = "default"
|
|
18
60
|
|
|
19
61
|
ctx_default_connection_name: ContextVar[str] = ContextVar(
|
|
20
62
|
"ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
|
|
@@ -69,13 +111,21 @@ async def providing_new_session(
|
|
|
69
111
|
connection_name: str | None = None,
|
|
70
112
|
) -> AsyncGenerator[AsyncSession, None]:
|
|
71
113
|
|
|
72
|
-
|
|
114
|
+
session_manager = use_session_manager()
|
|
115
|
+
current_session = session_manager.spawn_session(connection_name)
|
|
73
116
|
|
|
74
117
|
async with AsyncSession(
|
|
75
118
|
current_session.bind,
|
|
76
119
|
) as new_session, new_session.begin() as new_tx:
|
|
77
120
|
with providing_session(new_session, new_tx, connection_name):
|
|
78
|
-
|
|
121
|
+
try:
|
|
122
|
+
yield new_session
|
|
123
|
+
if new_tx.is_active:
|
|
124
|
+
await new_tx.commit()
|
|
125
|
+
except Exception:
|
|
126
|
+
if new_tx.is_active:
|
|
127
|
+
await new_tx.rollback()
|
|
128
|
+
raise
|
|
79
129
|
|
|
80
130
|
|
|
81
131
|
def use_session(connection_name: str | None = None) -> AsyncSession:
|
|
@@ -129,50 +179,7 @@ class AIOSQAConfig:
|
|
|
129
179
|
self.inject_default = inject_default
|
|
130
180
|
|
|
131
181
|
|
|
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):
|
|
182
|
+
class AIOSqlAlchemySessionInterceptor(AppInterceptor, SessionManager):
|
|
176
183
|
|
|
177
184
|
def __init__(self, config: AIOSQAConfig):
|
|
178
185
|
self.config = config
|
|
@@ -189,27 +196,33 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
|
|
|
189
196
|
self, app_context: AppTransactionContext
|
|
190
197
|
) -> AsyncGenerator[None, None]:
|
|
191
198
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
199
|
+
with providing_session_manager(self):
|
|
200
|
+
uses_connection_metadata = get_metadata_value(
|
|
201
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
202
|
+
connection_name=self.config.connection_name
|
|
203
|
+
),
|
|
204
|
+
self.config.inject_default,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not uses_connection_metadata:
|
|
208
|
+
yield
|
|
209
|
+
return
|
|
210
|
+
|
|
211
|
+
async with self.sessionmaker() as session, session.begin() as tx:
|
|
212
|
+
token = ctx_default_connection_name.set(self.config.connection_name)
|
|
213
|
+
with providing_session(session, tx, self.config.connection_name):
|
|
214
|
+
try:
|
|
215
|
+
yield
|
|
216
|
+
if tx.is_active:
|
|
217
|
+
await tx.commit()
|
|
218
|
+
except Exception as e:
|
|
219
|
+
await tx.rollback()
|
|
220
|
+
raise e
|
|
221
|
+
finally:
|
|
222
|
+
with suppress(ValueError):
|
|
223
|
+
ctx_default_connection_name.reset(token)
|
|
224
|
+
|
|
225
|
+
def spawn_session(self, connection_name: str | None = None) -> AsyncSession:
|
|
226
|
+
connection_name = ensure_name(connection_name)
|
|
227
|
+
session = self.sessionmaker()
|
|
228
|
+
return session
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
|
|
7
|
+
from jararaca.reflect.metadata import SetMetadata
|
|
8
|
+
|
|
9
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE = (
|
|
10
|
+
"inject_persistence_template_{connection_name}"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_use_persistence_session(
|
|
15
|
+
inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
|
|
16
|
+
) -> SetMetadata:
|
|
17
|
+
"""
|
|
18
|
+
Set whether to inject the connection metadata for the given connection name.
|
|
19
|
+
This is useful when you want to control whether the connection metadata
|
|
20
|
+
should be injected into the context or not.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
return SetMetadata(
|
|
24
|
+
INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
|
|
25
|
+
connection_name=connection_name
|
|
26
|
+
),
|
|
27
|
+
inject,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def uses_persistence_session(
|
|
32
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
33
|
+
) -> SetMetadata:
|
|
34
|
+
"""
|
|
35
|
+
Use connection metadata for the given connection name.
|
|
36
|
+
This is useful when you want to inject the connection metadata into the context,
|
|
37
|
+
for example, when you are using a specific connection for a specific operation.
|
|
38
|
+
"""
|
|
39
|
+
return set_use_persistence_session(True, connection_name=connection_name)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def skip_persistence_session(
|
|
43
|
+
connection_name: str = DEFAULT_CONNECTION_NAME,
|
|
44
|
+
) -> SetMetadata:
|
|
45
|
+
"""
|
|
46
|
+
Decorator to skip using connection metadata for the given connection name.
|
|
47
|
+
This is useful when you want to ensure that the connection metadata is not injected
|
|
48
|
+
into the context, for example, when you are using a different connection for a specific operation.
|
|
49
|
+
"""
|
|
50
|
+
return set_use_persistence_session(False, connection_name=connection_name)
|
jararaca/persistence/session.py
CHANGED