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.
Files changed (88) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +184 -12
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +272 -47
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +4 -0
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +33 -67
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
  23. jararaca/messagebus/message.py +4 -0
  24. jararaca/messagebus/publisher.py +6 -0
  25. jararaca/messagebus/worker.py +850 -383
  26. jararaca/microservice.py +110 -1
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +170 -13
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +4 -0
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +202 -11
  34. jararaca/persistence/base.py +38 -2
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +50 -20
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +88 -86
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +97 -45
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +4 -0
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +4 -0
  55. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +16 -10
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +34 -25
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +521 -105
  69. jararaca/scheduler/decorators.py +15 -22
  70. jararaca/scheduler/types.py +4 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +6 -2
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1074 -173
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +65 -39
  79. jararaca/utils/retry.py +10 -3
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  87. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  88. {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.microservice import AppTransactionContext, Container, Microservice
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
- get_tracing_ctx_provider,
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 __call__(
130
+ def start_span_context(
44
131
  self,
45
132
  trace_name: str,
46
- context_attributes: dict[str, str],
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
- if tx_data.context_type == "http":
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(name=title, context=ctx2):
94
- yield
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 = LoggingHandler(
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
 
@@ -1,4 +1,8 @@
1
- from typing import Any, Self, Type, TypeVar
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)
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from .base import BaseEntity
2
6
 
3
7
  __all__ = [
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -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.reflect.metadata import SetMetadata, get_metadata_value
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
- current_session = use_session(connection_name)
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
- yield new_session
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
- 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
- )
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
- 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):
206
- try:
207
- yield
208
- if tx.is_active:
209
- await tx.commit()
210
- except Exception as e:
211
- await tx.rollback()
212
- raise e
213
- finally:
214
- with suppress(ValueError):
215
- ctx_default_connection_name.reset(token)
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,5 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ DEFAULT_CONNECTION_NAME = "default"
@@ -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)
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  import re
2
6
  from datetime import date, datetime
3
7
  from functools import reduce