jararaca 0.4.0a5__py3-none-any.whl → 0.4.0a19__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.
- jararaca/__init__.py +9 -9
- jararaca/cli.py +643 -4
- jararaca/core/providers.py +4 -0
- jararaca/helpers/__init__.py +3 -0
- jararaca/helpers/global_scheduler/__init__.py +3 -0
- jararaca/helpers/global_scheduler/config.py +21 -0
- jararaca/helpers/global_scheduler/controller.py +42 -0
- jararaca/helpers/global_scheduler/registry.py +32 -0
- jararaca/messagebus/decorators.py +104 -10
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +50 -8
- jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
- jararaca/messagebus/interceptors/publisher_interceptor.py +25 -3
- jararaca/messagebus/worker.py +276 -200
- jararaca/microservice.py +3 -1
- jararaca/observability/providers/otel.py +31 -13
- jararaca/persistence/base.py +1 -1
- jararaca/persistence/utilities.py +47 -24
- jararaca/presentation/decorators.py +3 -3
- jararaca/reflect/decorators.py +24 -10
- jararaca/reflect/helpers.py +18 -0
- jararaca/rpc/http/__init__.py +2 -2
- jararaca/rpc/http/decorators.py +9 -9
- jararaca/scheduler/beat_worker.py +14 -14
- jararaca/tools/typescript/decorators.py +4 -4
- jararaca/tools/typescript/interface_parser.py +3 -1
- jararaca/utils/env_parse_utils.py +133 -0
- jararaca/utils/rabbitmq_utils.py +47 -0
- jararaca/utils/retry.py +11 -13
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +2 -1
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/RECORD +35 -27
- pyproject.toml +2 -1
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/LICENSE +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/LICENSES/GPL-3.0-or-later.txt +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +0 -0
- {jararaca-0.4.0a5.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
jararaca/microservice.py
CHANGED
|
@@ -59,6 +59,8 @@ class MessageBusTransactionData:
|
|
|
59
59
|
topic: str
|
|
60
60
|
message: MessageOf[Message]
|
|
61
61
|
message_type: Type[Message]
|
|
62
|
+
message_id: str | None = field(default=None)
|
|
63
|
+
processing_attempt: int = field(default=0)
|
|
62
64
|
context_type: Literal["message_bus"] = "message_bus"
|
|
63
65
|
|
|
64
66
|
|
|
@@ -309,7 +311,7 @@ class Container:
|
|
|
309
311
|
self.register(instance, bind_to)
|
|
310
312
|
return cast(T, instance)
|
|
311
313
|
|
|
312
|
-
def register(self, instance:
|
|
314
|
+
def register(self, instance: Any, bind_to: Any) -> None:
|
|
313
315
|
self.instances_map[bind_to] = instance
|
|
314
316
|
|
|
315
317
|
def get_by_type(self, token: Type[T]) -> T:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from contextlib import asynccontextmanager, contextmanager
|
|
7
|
-
from typing import Any, AsyncGenerator, Generator, Literal, Protocol
|
|
7
|
+
from typing import TYPE_CHECKING, Any, AsyncGenerator, Generator, Literal, Protocol
|
|
8
8
|
|
|
9
9
|
from opentelemetry import metrics, trace
|
|
10
10
|
from opentelemetry._logs import set_logger_provider
|
|
@@ -49,6 +49,10 @@ from jararaca.observability.decorators import (
|
|
|
49
49
|
)
|
|
50
50
|
from jararaca.observability.interceptor import ObservabilityProvider
|
|
51
51
|
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from opentelemetry.trace import Span as _Span
|
|
54
|
+
from typing_extensions import TypeIs
|
|
55
|
+
|
|
52
56
|
tracer: trace.Tracer = trace.get_tracer(__name__)
|
|
53
57
|
|
|
54
58
|
|
|
@@ -82,11 +86,13 @@ def extract_context_attributes(ctx: AppTransactionContext) -> dict[str, Any]:
|
|
|
82
86
|
}
|
|
83
87
|
elif tx_data.context_type == "message_bus":
|
|
84
88
|
extra_attributes = {
|
|
89
|
+
"bus.message.id": tx_data.message_id,
|
|
85
90
|
"bus.message.name": tx_data.message_type.__qualname__,
|
|
86
91
|
"bus.message.module": tx_data.message_type.__module__,
|
|
87
92
|
"bus.message.category": tx_data.message_type.MESSAGE_CATEGORY,
|
|
88
93
|
"bus.message.type": tx_data.message_type.MESSAGE_TYPE,
|
|
89
94
|
"bus.message.topic": tx_data.message_type.MESSAGE_TOPIC,
|
|
95
|
+
"bus.message.processing_attempt": tx_data.processing_attempt,
|
|
90
96
|
}
|
|
91
97
|
elif tx_data.context_type == "websocket":
|
|
92
98
|
extra_attributes = {
|
|
@@ -190,13 +196,13 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
|
190
196
|
|
|
191
197
|
@asynccontextmanager
|
|
192
198
|
async def root_setup(
|
|
193
|
-
self,
|
|
199
|
+
self, app_context: AppTransactionContext
|
|
194
200
|
) -> AsyncGenerator[None, None]:
|
|
195
201
|
|
|
196
202
|
title: str = "Unmapped App Context Execution"
|
|
197
203
|
headers: dict[str, Any] = {}
|
|
198
|
-
tx_data =
|
|
199
|
-
extra_attributes = extract_context_attributes(
|
|
204
|
+
tx_data = app_context.transaction_data
|
|
205
|
+
extra_attributes = extract_context_attributes(app_context)
|
|
200
206
|
|
|
201
207
|
if tx_data.context_type == "http":
|
|
202
208
|
headers = dict(tx_data.request.headers)
|
|
@@ -206,7 +212,7 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
|
206
212
|
].decode(errors="ignore")
|
|
207
213
|
|
|
208
214
|
elif tx_data.context_type == "message_bus":
|
|
209
|
-
title = f"Message Bus {tx_data.topic}"
|
|
215
|
+
title = f"Att#{tx_data.processing_attempt} Message Bus {tx_data.topic}"
|
|
210
216
|
headers = use_implicit_headers() or {}
|
|
211
217
|
|
|
212
218
|
elif tx_data.context_type == "websocket":
|
|
@@ -242,12 +248,12 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
|
|
|
242
248
|
) as root_span:
|
|
243
249
|
cx = root_span.get_span_context()
|
|
244
250
|
span_traceparent_id = hex(cx.trace_id)[2:].rjust(32, "0")
|
|
245
|
-
if
|
|
246
|
-
|
|
251
|
+
if app_context.transaction_data.context_type == "http":
|
|
252
|
+
app_context.transaction_data.request.scope[TRACEPARENT_KEY] = (
|
|
247
253
|
span_traceparent_id
|
|
248
254
|
)
|
|
249
|
-
elif
|
|
250
|
-
|
|
255
|
+
elif app_context.transaction_data.context_type == "websocket":
|
|
256
|
+
app_context.transaction_data.websocket.scope[TRACEPARENT_KEY] = (
|
|
251
257
|
span_traceparent_id
|
|
252
258
|
)
|
|
253
259
|
tracing_headers: ImplicitHeaders = {}
|
|
@@ -262,6 +268,16 @@ class LoggerHandlerCallback(Protocol):
|
|
|
262
268
|
def __call__(self, logger_handler: logging.Handler) -> None: ...
|
|
263
269
|
|
|
264
270
|
|
|
271
|
+
class SpanWithName(Protocol):
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def name(self) -> str: ...
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def is_span_with_name(span: Any) -> "TypeIs[SpanWithName]":
|
|
278
|
+
return hasattr(span, "name")
|
|
279
|
+
|
|
280
|
+
|
|
265
281
|
class CustomLoggingHandler(LoggingHandler):
|
|
266
282
|
|
|
267
283
|
def _translate(self, record: logging.LogRecord) -> dict[str, Any]:
|
|
@@ -270,14 +286,16 @@ class CustomLoggingHandler(LoggingHandler):
|
|
|
270
286
|
data = super()._translate(record)
|
|
271
287
|
extra_attributes = extract_context_attributes(ctx)
|
|
272
288
|
|
|
273
|
-
current_span = trace.get_current_span()
|
|
289
|
+
current_span: "_Span" = trace.get_current_span()
|
|
274
290
|
|
|
275
291
|
data["attributes"] = {
|
|
276
292
|
**data.get("attributes", {}),
|
|
277
293
|
**extra_attributes,
|
|
278
294
|
**(
|
|
279
295
|
{
|
|
280
|
-
"span_name":
|
|
296
|
+
"span_name": (
|
|
297
|
+
current_span.name if is_span_with_name(current_span) else ""
|
|
298
|
+
),
|
|
281
299
|
}
|
|
282
300
|
if hasattr(current_span, "name")
|
|
283
301
|
and current_span.is_recording() is False
|
|
@@ -298,7 +316,7 @@ class OtelObservabilityProvider(ObservabilityProvider):
|
|
|
298
316
|
logs_exporter: LogExporter,
|
|
299
317
|
span_exporter: SpanExporter,
|
|
300
318
|
meter_exporter: MeterExporter,
|
|
301
|
-
logging_handler_callback: LoggerHandlerCallback = lambda
|
|
319
|
+
logging_handler_callback: LoggerHandlerCallback = lambda logger_handler: None,
|
|
302
320
|
meter_export_interval: int = 5000,
|
|
303
321
|
) -> None:
|
|
304
322
|
self.app_name = app_name
|
|
@@ -356,7 +374,7 @@ class OtelObservabilityProvider(ObservabilityProvider):
|
|
|
356
374
|
def from_url(
|
|
357
375
|
app_name: str,
|
|
358
376
|
url: str,
|
|
359
|
-
logging_handler_callback: LoggerHandlerCallback = lambda
|
|
377
|
+
logging_handler_callback: LoggerHandlerCallback = lambda logger_handler: None,
|
|
360
378
|
meter_export_interval: int = 5000,
|
|
361
379
|
) -> "OtelObservabilityProvider":
|
|
362
380
|
"""
|
jararaca/persistence/base.py
CHANGED
|
@@ -43,7 +43,7 @@ BASED_BASE_ENTITY_T = TypeVar("BASED_BASE_ENTITY_T", bound="BaseEntity")
|
|
|
43
43
|
class BaseEntity(AsyncAttrs, DeclarativeBase):
|
|
44
44
|
|
|
45
45
|
@classmethod
|
|
46
|
-
def from_basemodel(cls, mutation:
|
|
46
|
+
def from_basemodel(cls, mutation: BaseModel) -> "Self":
|
|
47
47
|
intersection = set(cls.__annotations__.keys()) & set(
|
|
48
48
|
mutation.__class__.model_fields.keys()
|
|
49
49
|
)
|
|
@@ -321,6 +321,11 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
321
321
|
| Select[Tuple[QUERY_ENTITY_T]]
|
|
322
322
|
| None
|
|
323
323
|
) = None,
|
|
324
|
+
final_listing_statement: (
|
|
325
|
+
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
326
|
+
| None
|
|
327
|
+
) = None,
|
|
328
|
+
total_type: Literal["total_over", "count_subquery"] = "total_over",
|
|
324
329
|
) -> "Paginated[QUERY_ENTITY_T]":
|
|
325
330
|
"""
|
|
326
331
|
Executes a query with the provided filter and interceptors.
|
|
@@ -333,9 +338,9 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
333
338
|
|
|
334
339
|
initial_statement = self.base_statement or select(self.entity_type)
|
|
335
340
|
|
|
336
|
-
if base_statement and callable(base_statement):
|
|
341
|
+
if base_statement is not None and callable(base_statement):
|
|
337
342
|
initial_statement = base_statement(initial_statement)
|
|
338
|
-
elif base_statement and isinstance(base_statement, Select):
|
|
343
|
+
elif base_statement is not None and isinstance(base_statement, Select):
|
|
339
344
|
initial_statement = base_statement
|
|
340
345
|
|
|
341
346
|
tier_one_filtered_query = self.generate_filtered_query(
|
|
@@ -362,32 +367,50 @@ class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
|
362
367
|
)
|
|
363
368
|
)
|
|
364
369
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
)
|
|
368
|
-
paginated_query = paginated_query.limit(filter.page_size).offset(
|
|
369
|
-
(filter.page) * filter.page_size
|
|
370
|
-
)
|
|
370
|
+
if final_listing_statement is not None:
|
|
371
|
+
tier_two_filtered_query = final_listing_statement(tier_two_filtered_query)
|
|
371
372
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
373
|
+
if total_type == "total_over":
|
|
374
|
+
# Use window function for total count (single query)
|
|
375
|
+
paginated_query = tier_two_filtered_query.add_columns(
|
|
376
|
+
func.count().over().label("total_count")
|
|
377
|
+
)
|
|
378
|
+
paginated_query = paginated_query.limit(filter.page_size).offset(
|
|
379
|
+
(filter.page) * filter.page_size
|
|
380
|
+
)
|
|
375
381
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
382
|
+
result = await self.session.execute(paginated_query)
|
|
383
|
+
result = self.judge_unique(result)
|
|
384
|
+
rows = result.all()
|
|
385
|
+
|
|
386
|
+
if rows:
|
|
387
|
+
unpaginated_total = rows[0].total_count
|
|
388
|
+
result_scalars = [row[0] for row in rows]
|
|
383
389
|
else:
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
390
|
+
result_scalars = []
|
|
391
|
+
unpaginated_total = 0
|
|
392
|
+
else: # total_type == "count_subquery"
|
|
393
|
+
# Use separate count query (two queries)
|
|
394
|
+
paginated_query = tier_two_filtered_query.limit(filter.page_size).offset(
|
|
395
|
+
(filter.page) * filter.page_size
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
result = await self.session.execute(paginated_query)
|
|
399
|
+
result = self.judge_unique(result)
|
|
400
|
+
result_scalars = list(result.scalars())
|
|
401
|
+
|
|
402
|
+
# Always fetch total with separate query
|
|
403
|
+
unpaginated_total = (
|
|
404
|
+
await self.session.execute(
|
|
405
|
+
tier_two_filtered_query.with_only_columns(
|
|
406
|
+
func.count(self.entity_type.id)
|
|
407
|
+
).order_by(None)
|
|
408
|
+
if issubclass(self.entity_type, IdentifiableEntity)
|
|
409
|
+
else select(func.count()).select_from(
|
|
410
|
+
tier_two_filtered_query.subquery()
|
|
389
411
|
)
|
|
390
|
-
)
|
|
412
|
+
)
|
|
413
|
+
).scalar_one()
|
|
391
414
|
|
|
392
415
|
return Paginated(
|
|
393
416
|
items=result_scalars,
|
|
@@ -20,7 +20,7 @@ from jararaca.reflect.controller_inspect import (
|
|
|
20
20
|
ControllerMemberReflect,
|
|
21
21
|
inspect_controller,
|
|
22
22
|
)
|
|
23
|
-
from jararaca.reflect.decorators import
|
|
23
|
+
from jararaca.reflect.decorators import FUNC_OR_TYPE_T, StackableDecorator
|
|
24
24
|
|
|
25
25
|
DECORATED_TYPE = TypeVar("DECORATED_TYPE", bound=Any)
|
|
26
26
|
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=Callable[..., Any])
|
|
@@ -61,11 +61,11 @@ class RestController(StackableDecorator):
|
|
|
61
61
|
raise Exception("Router factory is not set")
|
|
62
62
|
return self.router_factory
|
|
63
63
|
|
|
64
|
-
def post_decorated(self, subject:
|
|
64
|
+
def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
65
65
|
|
|
66
66
|
def router_factory(
|
|
67
67
|
lifecycle: AppLifecycle,
|
|
68
|
-
instance:
|
|
68
|
+
instance: FUNC_OR_TYPE_T,
|
|
69
69
|
) -> APIRouter:
|
|
70
70
|
assert inspect.isclass(
|
|
71
71
|
subject
|
jararaca/reflect/decorators.py
CHANGED
|
@@ -2,23 +2,25 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
-
from typing import Any, Callable, Self, TypedDict, TypeVar, cast
|
|
5
|
+
from typing import Any, Callable, Generic, Self, TypedDict, TypeVar, cast
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
FUNC_OR_TYPE_T = Callable[..., Any] | type
|
|
8
8
|
|
|
9
|
+
DECORATED_T = TypeVar("DECORATED_T", bound="FUNC_OR_TYPE_T")
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
|
|
12
|
+
S = TypeVar("S", bound="BaseStackableDecorator")
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
class DecoratorMetadata(TypedDict):
|
|
14
|
-
decorators: "list[
|
|
15
|
-
decorators_by_type: "dict[Any, list[
|
|
16
|
+
decorators: "list[BaseStackableDecorator]"
|
|
17
|
+
decorators_by_type: "dict[Any, list[BaseStackableDecorator]]"
|
|
16
18
|
|
|
17
19
|
|
|
18
|
-
class
|
|
20
|
+
class BaseStackableDecorator:
|
|
19
21
|
_ATTR_NAME: str = "__jararaca_stackable_decorator__"
|
|
20
22
|
|
|
21
|
-
def __call__(self, subject:
|
|
23
|
+
def __call__(self, subject: Any) -> Any:
|
|
22
24
|
self.pre_decorated(subject)
|
|
23
25
|
self.register(subject, self)
|
|
24
26
|
self.post_decorated(subject)
|
|
@@ -45,7 +47,7 @@ class StackableDecorator:
|
|
|
45
47
|
return None
|
|
46
48
|
|
|
47
49
|
@classmethod
|
|
48
|
-
def register(cls, subject: Any, decorator: "
|
|
50
|
+
def register(cls, subject: Any, decorator: "BaseStackableDecorator") -> None:
|
|
49
51
|
if not cls._ATTR_NAME:
|
|
50
52
|
raise NotImplementedError("Subclasses must define _ATTR_NAME")
|
|
51
53
|
|
|
@@ -95,13 +97,13 @@ class StackableDecorator:
|
|
|
95
97
|
return decorators[-1]
|
|
96
98
|
return None
|
|
97
99
|
|
|
98
|
-
def pre_decorated(self, subject:
|
|
100
|
+
def pre_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
99
101
|
"""
|
|
100
102
|
Hook method called before the subject is decorated.
|
|
101
103
|
Can be overridden by subclasses to perform additional setup.
|
|
102
104
|
"""
|
|
103
105
|
|
|
104
|
-
def post_decorated(self, subject:
|
|
106
|
+
def post_decorated(self, subject: FUNC_OR_TYPE_T) -> None:
|
|
105
107
|
"""
|
|
106
108
|
Hook method called after the subject has been decorated.
|
|
107
109
|
Can be overridden by subclasses to perform additional setup.
|
|
@@ -148,6 +150,18 @@ class StackableDecorator:
|
|
|
148
150
|
)
|
|
149
151
|
|
|
150
152
|
|
|
153
|
+
class StackableDecorator(BaseStackableDecorator):
|
|
154
|
+
|
|
155
|
+
def __call__(self, subject: DECORATED_T) -> DECORATED_T:
|
|
156
|
+
return cast(DECORATED_T, super().__call__(subject))
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class GenericStackableDecorator(BaseStackableDecorator, Generic[DECORATED_T]):
|
|
160
|
+
|
|
161
|
+
def __call__(self, subject: DECORATED_T) -> DECORATED_T:
|
|
162
|
+
return cast(DECORATED_T, super().__call__(subject))
|
|
163
|
+
|
|
164
|
+
|
|
151
165
|
def resolve_class_decorators(
|
|
152
166
|
subject: Any, decorator_cls: type[S], inherit: bool = True
|
|
153
167
|
) -> list[S]:
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Lucas S
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from typing import TYPE_CHECKING, get_args, get_origin
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from types import GenericAlias
|
|
10
|
+
|
|
11
|
+
from typing_extensions import TypeIs
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def is_generic_alias(subject: object) -> "TypeIs[GenericAlias]":
|
|
15
|
+
origin = get_origin(subject)
|
|
16
|
+
args = get_args(subject)
|
|
17
|
+
|
|
18
|
+
return origin is not None and len(args) > 0
|
jararaca/rpc/http/__init__.py
CHANGED
|
@@ -43,9 +43,9 @@ from .decorators import ( # HTTP Method decorators; Request parameter decorator
|
|
|
43
43
|
ResponseMiddleware,
|
|
44
44
|
RestClient,
|
|
45
45
|
Retry,
|
|
46
|
-
RetryConfig,
|
|
47
46
|
RouteHttpErrorHandler,
|
|
48
47
|
RPCRequestNetworkError,
|
|
48
|
+
RPCRetryPolicy,
|
|
49
49
|
RPCUnhandleError,
|
|
50
50
|
Timeout,
|
|
51
51
|
TimeoutException,
|
|
@@ -85,7 +85,7 @@ __all__ = [
|
|
|
85
85
|
"RequestHook",
|
|
86
86
|
"ResponseHook",
|
|
87
87
|
# Configuration classes
|
|
88
|
-
"
|
|
88
|
+
"RPCRetryPolicy",
|
|
89
89
|
# Data structures
|
|
90
90
|
"HttpRPCRequest",
|
|
91
91
|
"HttpRPCResponse",
|
jararaca/rpc/http/decorators.py
CHANGED
|
@@ -27,7 +27,8 @@ from jararaca.reflect.decorators import StackableDecorator
|
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
FUNC_T = Callable[..., Awaitable[Any]]
|
|
31
|
+
DECORATED_FUNC = TypeVar("DECORATED_FUNC", bound=FUNC_T)
|
|
31
32
|
DECORATED_CLASS = TypeVar("DECORATED_CLASS", bound=Any)
|
|
32
33
|
|
|
33
34
|
|
|
@@ -143,11 +144,11 @@ class Timeout:
|
|
|
143
144
|
return func
|
|
144
145
|
|
|
145
146
|
@staticmethod
|
|
146
|
-
def get(func:
|
|
147
|
+
def get(func: FUNC_T) -> Optional["Timeout"]:
|
|
147
148
|
return getattr(func, Timeout.TIMEOUT_ATTR, None)
|
|
148
149
|
|
|
149
150
|
|
|
150
|
-
class
|
|
151
|
+
class RPCRetryPolicy:
|
|
151
152
|
"""Configuration for retry behavior"""
|
|
152
153
|
|
|
153
154
|
def __init__(
|
|
@@ -166,7 +167,7 @@ class Retry:
|
|
|
166
167
|
|
|
167
168
|
RETRY_ATTR = "__request_retry__"
|
|
168
169
|
|
|
169
|
-
def __init__(self, config:
|
|
170
|
+
def __init__(self, config: RPCRetryPolicy):
|
|
170
171
|
self.config = config
|
|
171
172
|
|
|
172
173
|
def __call__(self, func: DECORATED_FUNC) -> DECORATED_FUNC:
|
|
@@ -174,7 +175,7 @@ class Retry:
|
|
|
174
175
|
return func
|
|
175
176
|
|
|
176
177
|
@staticmethod
|
|
177
|
-
def get(func:
|
|
178
|
+
def get(func: FUNC_T) -> Optional["Retry"]:
|
|
178
179
|
return getattr(func, Retry.RETRY_ATTR, None)
|
|
179
180
|
|
|
180
181
|
|
|
@@ -191,7 +192,7 @@ class ContentType:
|
|
|
191
192
|
return func
|
|
192
193
|
|
|
193
194
|
@staticmethod
|
|
194
|
-
def get(func:
|
|
195
|
+
def get(func: FUNC_T) -> Optional["ContentType"]:
|
|
195
196
|
return getattr(func, ContentType.CONTENT_TYPE_ATTR, None)
|
|
196
197
|
|
|
197
198
|
|
|
@@ -390,7 +391,7 @@ class HttpRpcClientBuilder:
|
|
|
390
391
|
self._response_hooks = response_hooks
|
|
391
392
|
|
|
392
393
|
async def _execute_with_retry(
|
|
393
|
-
self, request: HttpRPCRequest, retry_config: Optional[
|
|
394
|
+
self, request: HttpRPCRequest, retry_config: Optional[RPCRetryPolicy]
|
|
394
395
|
) -> HttpRPCResponse:
|
|
395
396
|
"""Execute request with retry logic"""
|
|
396
397
|
if not retry_config:
|
|
@@ -660,7 +661,7 @@ __all__ = [
|
|
|
660
661
|
"FormData",
|
|
661
662
|
"File",
|
|
662
663
|
"Timeout",
|
|
663
|
-
"
|
|
664
|
+
"RPCRetryPolicy",
|
|
664
665
|
"Retry",
|
|
665
666
|
"ContentType",
|
|
666
667
|
"RestClient",
|
|
@@ -679,7 +680,6 @@ __all__ = [
|
|
|
679
680
|
"BasicAuth",
|
|
680
681
|
"ApiKeyAuth",
|
|
681
682
|
"CacheMiddleware",
|
|
682
|
-
"TracedRequestMiddleware",
|
|
683
683
|
"GlobalHttpErrorHandler",
|
|
684
684
|
"RouteHttpErrorHandler",
|
|
685
685
|
"HandleHttpErrorCallback",
|
|
@@ -43,7 +43,7 @@ from jararaca.scheduler.decorators import (
|
|
|
43
43
|
)
|
|
44
44
|
from jararaca.scheduler.types import DelayedMessageData
|
|
45
45
|
from jararaca.utils.rabbitmq_utils import RabbitmqUtils
|
|
46
|
-
from jararaca.utils.retry import
|
|
46
|
+
from jararaca.utils.retry import RetryPolicy, retry_with_backoff
|
|
47
47
|
|
|
48
48
|
logger = logging.getLogger(__name__)
|
|
49
49
|
|
|
@@ -168,7 +168,7 @@ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
|
168
168
|
|
|
169
169
|
return await retry_with_backoff(
|
|
170
170
|
_establish_connection,
|
|
171
|
-
|
|
171
|
+
retry_policy=self.config.connection_retry_config,
|
|
172
172
|
retry_exceptions=(
|
|
173
173
|
AMQPConnectionError,
|
|
174
174
|
ConnectionError,
|
|
@@ -189,7 +189,7 @@ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
|
189
189
|
|
|
190
190
|
return await retry_with_backoff(
|
|
191
191
|
_establish_channel,
|
|
192
|
-
|
|
192
|
+
retry_policy=self.config.connection_retry_config,
|
|
193
193
|
retry_exceptions=(
|
|
194
194
|
AMQPConnectionError,
|
|
195
195
|
AMQPChannelError,
|
|
@@ -219,7 +219,7 @@ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
|
219
219
|
try:
|
|
220
220
|
await retry_with_backoff(
|
|
221
221
|
_dispatch,
|
|
222
|
-
|
|
222
|
+
retry_policy=self.config.dispatch_retry_config,
|
|
223
223
|
retry_exceptions=(
|
|
224
224
|
AMQPConnectionError,
|
|
225
225
|
AMQPChannelError,
|
|
@@ -261,13 +261,13 @@ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
|
261
261
|
aio_pika.Message(
|
|
262
262
|
body=delayed_message.payload,
|
|
263
263
|
),
|
|
264
|
-
routing_key=f"{delayed_message.message_topic}
|
|
264
|
+
routing_key=f"{delayed_message.message_topic}.#",
|
|
265
265
|
)
|
|
266
266
|
|
|
267
267
|
try:
|
|
268
268
|
await retry_with_backoff(
|
|
269
269
|
_dispatch,
|
|
270
|
-
|
|
270
|
+
retry_policy=self.config.dispatch_retry_config,
|
|
271
271
|
retry_exceptions=(
|
|
272
272
|
AMQPConnectionError,
|
|
273
273
|
AMQPChannelError,
|
|
@@ -306,7 +306,7 @@ class _RabbitMQBrokerDispatcher(_MessageBrokerDispatcher):
|
|
|
306
306
|
logger.debug("Initializing RabbitMQ connection...")
|
|
307
307
|
await retry_with_backoff(
|
|
308
308
|
_initialize,
|
|
309
|
-
|
|
309
|
+
retry_policy=self.config.connection_retry_config,
|
|
310
310
|
retry_exceptions=(
|
|
311
311
|
AMQPConnectionError,
|
|
312
312
|
AMQPChannelError,
|
|
@@ -447,8 +447,8 @@ def _get_message_broker_dispatcher_from_url(
|
|
|
447
447
|
class BeatWorkerConfig:
|
|
448
448
|
"""Configuration for beat worker connection resilience"""
|
|
449
449
|
|
|
450
|
-
connection_retry_config:
|
|
451
|
-
default_factory=lambda:
|
|
450
|
+
connection_retry_config: RetryPolicy = field(
|
|
451
|
+
default_factory=lambda: RetryPolicy(
|
|
452
452
|
max_retries=10,
|
|
453
453
|
initial_delay=2.0,
|
|
454
454
|
max_delay=60.0,
|
|
@@ -456,8 +456,8 @@ class BeatWorkerConfig:
|
|
|
456
456
|
jitter=True,
|
|
457
457
|
)
|
|
458
458
|
)
|
|
459
|
-
dispatch_retry_config:
|
|
460
|
-
default_factory=lambda:
|
|
459
|
+
dispatch_retry_config: RetryPolicy = field(
|
|
460
|
+
default_factory=lambda: RetryPolicy(
|
|
461
461
|
max_retries=3,
|
|
462
462
|
initial_delay=1.0,
|
|
463
463
|
max_delay=10.0,
|
|
@@ -561,7 +561,7 @@ class BeatWorker:
|
|
|
561
561
|
|
|
562
562
|
# Ensure we have a healthy connection before starting the main loop
|
|
563
563
|
if (
|
|
564
|
-
|
|
564
|
+
isinstance(self.broker, _RabbitMQBrokerDispatcher)
|
|
565
565
|
and not self.broker.connection_healthy
|
|
566
566
|
):
|
|
567
567
|
logger.error("Connection not healthy at start of processing loop. Exiting.")
|
|
@@ -570,7 +570,7 @@ class BeatWorker:
|
|
|
570
570
|
while not self.shutdown_event.is_set():
|
|
571
571
|
# Check connection health before processing scheduled actions
|
|
572
572
|
if (
|
|
573
|
-
|
|
573
|
+
isinstance(self.broker, _RabbitMQBrokerDispatcher)
|
|
574
574
|
and not self.broker.connection_healthy
|
|
575
575
|
):
|
|
576
576
|
logger.error("Broker connection is not healthy. Exiting.")
|
|
@@ -729,7 +729,7 @@ class BeatWorker:
|
|
|
729
729
|
|
|
730
730
|
# Check if broker connection is healthy
|
|
731
731
|
if (
|
|
732
|
-
|
|
732
|
+
isinstance(self.broker, _RabbitMQBrokerDispatcher)
|
|
733
733
|
and self.broker.connection_healthy
|
|
734
734
|
):
|
|
735
735
|
logger.debug("Broker connection is healthy")
|
|
@@ -67,11 +67,11 @@ class SplitInputOutput(StackableDecorator):
|
|
|
67
67
|
pass
|
|
68
68
|
|
|
69
69
|
@staticmethod
|
|
70
|
-
def is_split_model(
|
|
70
|
+
def is_split_model(cls_type: type) -> bool:
|
|
71
71
|
"""
|
|
72
72
|
Check if the Pydantic model is marked for split interface generation.
|
|
73
73
|
"""
|
|
74
|
-
return SplitInputOutput.get_last(
|
|
74
|
+
return SplitInputOutput.get_last(cls_type) is not None
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
class ExposeType:
|
|
@@ -106,11 +106,11 @@ class ExposeType:
|
|
|
106
106
|
return cls
|
|
107
107
|
|
|
108
108
|
@staticmethod
|
|
109
|
-
def is_exposed_type(
|
|
109
|
+
def is_exposed_type(cls_type: type) -> bool:
|
|
110
110
|
"""
|
|
111
111
|
Check if the type is marked for explicit exposure.
|
|
112
112
|
"""
|
|
113
|
-
return getattr(
|
|
113
|
+
return getattr(cls_type, ExposeType.METADATA_KEY, False)
|
|
114
114
|
|
|
115
115
|
@staticmethod
|
|
116
116
|
def get_all_exposed_types() -> set[type]:
|
|
@@ -23,6 +23,7 @@ from typing import (
|
|
|
23
23
|
Literal,
|
|
24
24
|
Type,
|
|
25
25
|
TypeVar,
|
|
26
|
+
cast,
|
|
26
27
|
get_origin,
|
|
27
28
|
)
|
|
28
29
|
from uuid import UUID
|
|
@@ -554,7 +555,8 @@ def parse_single_typescript_interface(
|
|
|
554
555
|
mapped_types.update(inherited_classes)
|
|
555
556
|
|
|
556
557
|
if Enum in inherited_classes:
|
|
557
|
-
|
|
558
|
+
enum_casted = cast(Type[Enum], basemodel_type)
|
|
559
|
+
enum_values = sorted([(x._name_, x.value) for x in enum_casted])
|
|
558
560
|
return (
|
|
559
561
|
set(),
|
|
560
562
|
f"export enum {basemodel_type.__name__}{interface_suffix} {{\n"
|