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/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 SchedulerAppContext:
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 HttpAppContext:
41
+ class HttpTransactionData:
42
42
  request: Request
43
43
  context_type: Literal["http"] = "http"
44
44
 
45
45
 
46
46
  @dataclass
47
- class MessageBusAppContext:
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 WebSocketAppContext:
54
+ class WebSocketTransactionData:
55
55
  websocket: WebSocket
56
56
  context_type: Literal["websocket"] = "websocket"
57
57
 
58
58
 
59
- AppContext = (
60
- MessageBusAppContext | HttpAppContext | SchedulerAppContext | WebSocketAppContext
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
- def use_app_context() -> AppContext:
67
- return app_context_ctxvar.get()
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(app_context: AppContext) -> Generator[None, None, None]:
72
- token = app_context_ctxvar.set(app_context)
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
- app_context_ctxvar.reset(token)
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(self, app_context: AppContext) -> AsyncContextManager[None]: ...
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
- "AppContext",
269
+ "AppTransactionContext",
231
270
  "AppInterceptor",
232
271
  "AppInterceptorWithLifecycle",
233
272
  "Container",
234
273
  "Microservice",
235
- "SchedulerAppContext",
236
- "WebSocketAppContext",
237
- "app_context_ctxvar",
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 AppContext
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(self, app_context: AppContext) -> AsyncContextManager[None]: ...
31
+ def root_setup(
32
+ self, app_context: AppTransactionContext
33
+ ) -> AsyncContextManager[None]: ...
32
34
 
33
- def provide_provider(self, app_context: AppContext) -> TracingContextProvider: ...
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(self, app_context: AppContext) -> AsyncGenerator[None, None]:
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 AppContext, Container, Microservice
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: AppContext) -> None:
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(self, app_context: AppContext) -> TracingContextProvider:
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(self, app_context: AppContext) -> AsyncGenerator[None, None]:
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
- if app_context.context_type == "http":
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 app_context.context_type == "message_bus":
70
- title = f"Message Bus {app_context.topic}"
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
@@ -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 AsyncSession, async_sessionmaker, create_async_engine
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 AppContext, AppInterceptor
14
+ from jararaca.microservice import AppInterceptor, AppTransactionContext
15
+ from jararaca.reflect.metadata import SetMetadata, get_metadata_value
10
16
 
11
- ctx_session_map = ContextVar[dict[str, AsyncSession]]("ctx_session_map", default={})
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 provide_session(
16
- connection_name: str, session: AsyncSession
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({**current_map, connection_name: session})
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
- def use_session(connection_name: str = "default") -> AsyncSession:
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(f"Session not found for connection {connection_name}")
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(self, app_context: AppContext) -> AsyncGenerator[None, None]:
57
- async with self.sessionmaker() as session:
58
- with provide_session(self.config.connection_name, session):
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
- await session.commit()
208
+ if tx.is_active:
209
+ await tx.commit()
62
210
  except Exception as e:
63
- await session.rollback()
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 inspect
2
- from typing import Any, Callable, Literal, Protocol, TypeVar, cast
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 = inspect.getmembers(cls, predicate=inspect.isfunction)
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=getattr(instance, name),
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=getattr(instance, name),
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
@@ -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 HttpAppContext, WebSocketAppContext
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
- HttpAppContext(request=request)
84
- if request
85
- else WebSocketAppContext(websocket=websocket)
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