jararaca 0.3.11a4__py3-none-any.whl → 0.3.11a5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of jararaca might be problematic. Click here for more details.

@@ -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
@@ -11,15 +11,18 @@ from sqlalchemy.ext.asyncio import (
11
11
  )
12
12
  from sqlalchemy.ext.asyncio.engine import AsyncEngine
13
13
 
14
- from jararaca.microservice import AppContext, AppInterceptor
14
+ from jararaca.microservice import AppInterceptor, AppTransactionContext
15
+ from jararaca.reflect.metadata import SetMetadata, get_metadata_value
15
16
 
16
- ctx_default_connection_name: ContextVar[str] = ContextVar("ctx_default_connection_name")
17
+ DEFAULT_CONNECTION_NAME = "default"
18
+
19
+ ctx_default_connection_name: ContextVar[str] = ContextVar(
20
+ "ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
21
+ )
17
22
 
18
23
 
19
24
  def ensure_name(name: str | None) -> str:
20
- if name is None:
21
- return ctx_default_connection_name.get()
22
- return name
25
+ return ctx_default_connection_name.get()
23
26
 
24
27
 
25
28
  @dataclass
@@ -37,7 +40,10 @@ def providing_session(
37
40
  tx: AsyncSessionTransaction,
38
41
  connection_name: str | None = None,
39
42
  ) -> Generator[None, Any, None]:
40
-
43
+ """
44
+ Context manager to provide a session and transaction for a specific connection name.
45
+ If no connection name is provided, it uses the default connection name from the context variable.
46
+ """
41
47
  connection_name = ensure_name(connection_name)
42
48
  current_map = ctx_session_map.get({})
43
49
 
@@ -52,6 +58,12 @@ def providing_session(
52
58
  ctx_session_map.reset(token)
53
59
 
54
60
 
61
+ provide_session = providing_session
62
+ """
63
+ Alias for `providing_session` to maintain backward compatibility.
64
+ """
65
+
66
+
55
67
  @asynccontextmanager
56
68
  async def providing_new_session(
57
69
  connection_name: str | None = None,
@@ -70,7 +82,9 @@ def use_session(connection_name: str | None = None) -> AsyncSession:
70
82
  connection_name = ensure_name(connection_name)
71
83
  current_map = ctx_session_map.get({})
72
84
  if connection_name not in current_map:
73
- raise ValueError(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
+ )
74
88
 
75
89
  return current_map[connection_name].session
76
90
 
@@ -102,10 +116,60 @@ def use_transaction(connection_name: str | None = None) -> AsyncSessionTransacti
102
116
  class AIOSQAConfig:
103
117
  url: str | AsyncEngine
104
118
  connection_name: str
105
-
106
- def __init__(self, url: str | AsyncEngine, connection_name: str = "default"):
119
+ inject_default: bool
120
+
121
+ def __init__(
122
+ self,
123
+ url: str | AsyncEngine,
124
+ connection_name: str = DEFAULT_CONNECTION_NAME,
125
+ inject_default: bool = True,
126
+ ):
107
127
  self.url = url
108
128
  self.connection_name = connection_name
129
+ self.inject_default = inject_default
130
+
131
+
132
+ INJECT_CONNECTION_METADATA = "inject_connection_metadata_{connection_name}"
133
+
134
+
135
+ def set_inject_connection(
136
+ inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
137
+ ) -> SetMetadata:
138
+ """
139
+ Set whether to inject the connection metadata for the given connection name.
140
+ This is useful when you want to control whether the connection metadata
141
+ should be injected into the context or not.
142
+ """
143
+
144
+ return SetMetadata(
145
+ INJECT_CONNECTION_METADATA.format(connection_name=connection_name), inject
146
+ )
147
+
148
+
149
+ def uses_connection(
150
+ connection_name: str = DEFAULT_CONNECTION_NAME,
151
+ ) -> SetMetadata:
152
+ """
153
+ Use connection metadata for the given connection name.
154
+ This is useful when you want to inject the connection metadata into the context,
155
+ for example, when you are using a specific connection for a specific operation.
156
+ """
157
+ return SetMetadata(
158
+ INJECT_CONNECTION_METADATA.format(connection_name=connection_name), True
159
+ )
160
+
161
+
162
+ def dnt_uses_connection(
163
+ connection_name: str = DEFAULT_CONNECTION_NAME,
164
+ ) -> SetMetadata:
165
+ """
166
+ Do not use connection metadata for the given connection name.
167
+ This is useful when you want to ensure that the connection metadata is not injected
168
+ into the context, for example, when you are using a different connection for a specific operation.
169
+ """
170
+ return SetMetadata(
171
+ INJECT_CONNECTION_METADATA.format(connection_name=connection_name), False
172
+ )
109
173
 
110
174
 
111
175
  class AIOSqlAlchemySessionInterceptor(AppInterceptor):
@@ -121,7 +185,20 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
121
185
  self.sessionmaker = async_sessionmaker(self.engine)
122
186
 
123
187
  @asynccontextmanager
124
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
188
+ async def intercept(
189
+ self, app_context: AppTransactionContext
190
+ ) -> AsyncGenerator[None, None]:
191
+
192
+ uses_connection_metadata = get_metadata_value(
193
+ INJECT_CONNECTION_METADATA.format(
194
+ connection_name=self.config.connection_name
195
+ ),
196
+ self.config.inject_default,
197
+ )
198
+
199
+ if not uses_connection_metadata:
200
+ yield
201
+ return
125
202
 
126
203
  async with self.sessionmaker() as session, session.begin() as tx:
127
204
  token = ctx_default_connection_name.set(self.config.connection_name)
@@ -134,4 +211,5 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
134
211
  await tx.rollback()
135
212
  raise e
136
213
  finally:
137
- ctx_default_connection_name.reset(token)
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
 
@@ -13,9 +13,9 @@ from pydantic import BaseModel
13
13
  from jararaca.core.uow import UnitOfWorkContextProvider
14
14
  from jararaca.di import Container
15
15
  from jararaca.microservice import (
16
- AppContext,
17
16
  AppInterceptor,
18
17
  AppInterceptorWithLifecycle,
18
+ AppTransactionContext,
19
19
  Microservice,
20
20
  )
21
21
  from jararaca.presentation.decorators import (
@@ -185,7 +185,9 @@ class WebSocketInterceptor(AppInterceptor, AppInterceptorWithLifecycle):
185
185
  self.shutdown_event.set()
186
186
 
187
187
  @asynccontextmanager
188
- async def intercept(self, app_context: AppContext) -> AsyncGenerator[None, None]:
188
+ async def intercept(
189
+ self, app_context: AppTransactionContext
190
+ ) -> AsyncGenerator[None, None]:
189
191
 
190
192
  staging_ws_messages_sender = StagingWebSocketMessageSender(
191
193
  self.connection_manager
File without changes
@@ -0,0 +1,75 @@
1
+ import inspect
2
+ from dataclasses import dataclass
3
+ from typing import Any, Callable, Mapping, Tuple, Type
4
+
5
+ from frozendict import frozendict
6
+
7
+ from jararaca.reflect.metadata import ControllerInstanceMetadata, SetMetadata
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ControllerReflect:
12
+
13
+ controller_class: Type[Any]
14
+ metadata: Mapping[str, ControllerInstanceMetadata]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ControllerMemberReflect:
19
+ controller_reflect: ControllerReflect
20
+ member_function: Callable[..., Any]
21
+ metadata: Mapping[str, ControllerInstanceMetadata]
22
+
23
+
24
+ def inspect_controller(
25
+ controller: Type[Any],
26
+ ) -> Tuple[ControllerReflect, Mapping[str, ControllerMemberReflect]]:
27
+ """
28
+ Inspect a controller class to extract its metadata and member functions.
29
+
30
+ Args:
31
+ controller (Type[Any]): The controller class to inspect.
32
+
33
+ Returns:
34
+ Tuple[ControllerReflect, list[ControllerMemberReflect]]: A tuple containing the controller reflect and a list of member reflects.
35
+ """
36
+ controller_metadata_list = SetMetadata.get(controller)
37
+
38
+ controller_metadata_map = frozendict(
39
+ {
40
+ metadata.key: ControllerInstanceMetadata(
41
+ value=metadata.value, inherited=False
42
+ )
43
+ for metadata in controller_metadata_list
44
+ }
45
+ )
46
+
47
+ controller_reflect = ControllerReflect(
48
+ controller_class=controller, metadata=controller_metadata_map
49
+ )
50
+
51
+ members = {
52
+ name: ControllerMemberReflect(
53
+ controller_reflect=controller_reflect,
54
+ member_function=member,
55
+ metadata=frozendict(
56
+ {
57
+ **{
58
+ key: ControllerInstanceMetadata(
59
+ value=value.value, inherited=True
60
+ )
61
+ for key, value in controller_metadata_map.items()
62
+ },
63
+ **{
64
+ metadata.key: ControllerInstanceMetadata(
65
+ value=metadata.value, inherited=False
66
+ )
67
+ for metadata in SetMetadata.get(member)
68
+ },
69
+ }
70
+ ),
71
+ )
72
+ for name, member in inspect.getmembers(controller, predicate=inspect.isfunction)
73
+ }
74
+
75
+ return controller_reflect, members
@@ -1,19 +1,39 @@
1
1
  from contextlib import contextmanager, suppress
2
2
  from contextvars import ContextVar
3
- from typing import Any, Awaitable, Callable, TypeVar, Union, cast
3
+ from dataclasses import dataclass
4
+ from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
4
5
 
5
6
  DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
6
7
 
7
8
 
8
- metadata_context: ContextVar[dict[str, Any]] = ContextVar("metadata_context")
9
+ @dataclass
10
+ class ControllerInstanceMetadata:
11
+ value: Any
12
+ inherited: bool
9
13
 
10
14
 
11
- def get_metadata(key: str) -> Any | None:
15
+ metadata_context: ContextVar[Mapping[str, ControllerInstanceMetadata]] = ContextVar(
16
+ "metadata_context"
17
+ )
18
+
19
+
20
+ def get_metadata(key: str) -> ControllerInstanceMetadata | None:
12
21
  return metadata_context.get({}).get(key)
13
22
 
14
23
 
24
+ def get_metadata_value(key: str, default: Any | None = None) -> Any:
25
+ metadata = get_metadata(key)
26
+ if metadata is None:
27
+ return default
28
+ return metadata.value
29
+
30
+
31
+ def get_all_metadata() -> Mapping[str, ControllerInstanceMetadata]:
32
+ return metadata_context.get({})
33
+
34
+
15
35
  @contextmanager
16
- def provide_metadata(metadata: dict[str, Any]) -> Any:
36
+ def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
17
37
 
18
38
  current_metadata = metadata_context.get({})
19
39
 
@@ -39,7 +59,7 @@ class SetMetadata:
39
59
  setattr(cls, SetMetadata.METATADA_LIST, metadata_list)
40
60
 
41
61
  @staticmethod
42
- def get(cls: type) -> "list[SetMetadata]":
62
+ def get(cls: DECORATED) -> "list[SetMetadata]":
43
63
  return cast(list[SetMetadata], getattr(cls, SetMetadata.METATADA_LIST, []))
44
64
 
45
65
  def __call__(self, cls: DECORATED) -> DECORATED:
@@ -1,5 +1,11 @@
1
1
  import inspect
2
- from typing import Any, Callable, TypeVar, cast
2
+ from dataclasses import dataclass
3
+ from typing import Any, Awaitable, Callable, TypeVar, cast
4
+
5
+ from jararaca.reflect.controller_inspect import (
6
+ ControllerMemberReflect,
7
+ inspect_controller,
8
+ )
3
9
 
4
10
  DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
5
11
 
@@ -66,25 +72,6 @@ class ScheduledAction:
66
72
  ScheduledAction, getattr(func, ScheduledAction.SCHEDULED_ACTION_ATTR)
67
73
  )
68
74
 
69
- @staticmethod
70
- def get_type_scheduled_actions(
71
- instance: Any,
72
- ) -> list[tuple[Callable[..., Any], "ScheduledAction"]]:
73
-
74
- members = inspect.getmembers(instance, predicate=inspect.ismethod)
75
-
76
- scheduled_actions: list[tuple[Callable[..., Any], "ScheduledAction"]] = []
77
-
78
- for _, member in members:
79
- scheduled_action = ScheduledAction.get_scheduled_action(member)
80
-
81
- if scheduled_action is None:
82
- continue
83
-
84
- scheduled_actions.append((member, scheduled_action))
85
-
86
- return scheduled_actions
87
-
88
75
  @staticmethod
89
76
  def get_function_id(
90
77
  func: Callable[..., Any],
@@ -94,3 +81,44 @@ class ScheduledAction:
94
81
  This is used to identify the scheduled action in the message broker.
95
82
  """
96
83
  return f"{func.__module__}.{func.__qualname__}"
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class ScheduledActionData:
88
+ spec: ScheduledAction
89
+ controller_member: ControllerMemberReflect
90
+ callable: Callable[..., Awaitable[None]]
91
+
92
+
93
+ def get_type_scheduled_actions(
94
+ instance: Any,
95
+ ) -> list[ScheduledActionData]:
96
+
97
+ _, member_metadata_map = inspect_controller(instance.__class__)
98
+
99
+ members = inspect.getmembers(instance, predicate=inspect.ismethod)
100
+
101
+ scheduled_actions: list[ScheduledActionData] = []
102
+
103
+ for name, member in members:
104
+ scheduled_action = ScheduledAction.get_scheduled_action(member)
105
+
106
+ if scheduled_action is None:
107
+ continue
108
+
109
+ if name not in member_metadata_map:
110
+ raise Exception(
111
+ f"Member '{name}' is not a valid controller member in '{instance.__class__.__name__}'"
112
+ )
113
+
114
+ member_metadata = member_metadata_map[name]
115
+
116
+ scheduled_actions.append(
117
+ ScheduledActionData(
118
+ callable=member,
119
+ spec=scheduled_action,
120
+ controller_member=member_metadata,
121
+ )
122
+ )
123
+
124
+ return scheduled_actions