jararaca 0.3.17__py3-none-any.whl → 0.3.19__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/__init__.py CHANGED
@@ -17,6 +17,8 @@ if TYPE_CHECKING:
17
17
  AppTransactionContext,
18
18
  use_app_type,
19
19
  )
20
+ from jararaca.observability.decorators import TracedClass, TracedFunc, traced_class
21
+ from jararaca.observability.hooks import spawn_trace
20
22
  from jararaca.observability.interceptor import ObservabilityInterceptor
21
23
  from jararaca.observability.providers.otel import OtelObservabilityProvider
22
24
  from jararaca.persistence.sort_filter import (
@@ -222,6 +224,10 @@ if TYPE_CHECKING:
222
224
  "HttpPut",
223
225
  "HttpDelete",
224
226
  "ObservabilityInterceptor",
227
+ "TracedFunc",
228
+ "TracedClass",
229
+ "traced_class",
230
+ "spawn_trace",
225
231
  "QueryInjector",
226
232
  "HttpMicroservice",
227
233
  "use_current_container",
@@ -392,6 +398,10 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
392
398
  "HttpPut": (__SPEC_PARENT__, "rpc.http.decorators", "Put"),
393
399
  "HttpDelete": (__SPEC_PARENT__, "rpc.http.decorators", "Delete"),
394
400
  "ObservabilityInterceptor": (__SPEC_PARENT__, "observability.interceptor", None),
401
+ "TracedFunc": (__SPEC_PARENT__, "observability.decorators", None),
402
+ "TracedClass": (__SPEC_PARENT__, "observability.decorators", None),
403
+ "traced_class": (__SPEC_PARENT__, "observability.decorators", None),
404
+ "spawn_trace": (__SPEC_PARENT__, "observability.hooks", None),
395
405
  "QueryInjector": (__SPEC_PARENT__, "persistence.utilities", None),
396
406
  "HttpMicroservice": (__SPEC_PARENT__, "presentation.http_microservice", None),
397
407
  "use_current_container": (__SPEC_PARENT__, "microservice", None),
jararaca/core/uow.py CHANGED
@@ -33,7 +33,6 @@ class UnitOfWorkContextProvider:
33
33
  self.container = container
34
34
  self.container_interceptor = ContainerInterceptor(container)
35
35
 
36
- # TODO: Guarantee that the context is closed whenever an exception is raised
37
36
  # TODO: Guarantee a unit of work workflow for the whole request, including all the interceptors
38
37
 
39
38
  def factory_app_interceptors(self) -> Sequence[AppInterceptor]:
@@ -65,7 +64,36 @@ class UnitOfWorkContextProvider:
65
64
  for ctx in ctxs:
66
65
  await ctx.__aenter__()
67
66
 
68
- yield None
69
-
70
- for ctx in reversed(ctxs):
71
- await ctx.__aexit__(None, None, None)
67
+ exc_type = None
68
+ exc_value = None
69
+ exc_traceback = None
70
+
71
+ try:
72
+ yield None
73
+ except BaseException as e:
74
+ exc_type = type(e)
75
+ exc_value = e
76
+ exc_traceback = e.__traceback__
77
+ raise
78
+ finally:
79
+ # Exit interceptors in reverse order, propagating exception info
80
+ for ctx in reversed(ctxs):
81
+ try:
82
+ suppressed = await ctx.__aexit__(
83
+ exc_type, exc_value, exc_traceback
84
+ )
85
+ # If an interceptor returns True, it suppresses the exception
86
+ if suppressed and exc_type is not None:
87
+ exc_type = None
88
+ exc_value = None
89
+ exc_traceback = None
90
+ except BaseException as exit_exc:
91
+ # If an interceptor raises an exception during cleanup,
92
+ # replace the original exception with the new one
93
+ exc_type = type(exit_exc)
94
+ exc_value = exit_exc
95
+ exc_traceback = exit_exc.__traceback__
96
+
97
+ # Re-raise the exception if it wasn't suppressed
98
+ if exc_type is not None and exc_value is not None:
99
+ raise exc_value.with_traceback(exc_traceback)
@@ -0,0 +1,45 @@
1
+ import datetime
2
+ import decimal
3
+ import typing
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import Any, Dict, Generator
7
+
8
+ FieldArray = list["FieldValue"]
9
+ """A data structure for holding an array of field values."""
10
+
11
+ FieldTable = typing.Dict[str, "FieldValue"]
12
+ FieldValue = (
13
+ bool
14
+ | bytes
15
+ | bytearray
16
+ | decimal.Decimal
17
+ | FieldArray
18
+ | FieldTable
19
+ | float
20
+ | int
21
+ | None
22
+ | str
23
+ | datetime.datetime
24
+ )
25
+
26
+ ImplicitHeaders = Dict[str, FieldValue]
27
+
28
+ implicit_headers_ctx = ContextVar[ImplicitHeaders | None](
29
+ "implicit_headers_ctx", default=None
30
+ )
31
+
32
+
33
+ def use_implicit_headers() -> ImplicitHeaders | None:
34
+ return implicit_headers_ctx.get()
35
+
36
+
37
+ @contextmanager
38
+ def provide_implicit_headers(
39
+ implicit_headers: ImplicitHeaders,
40
+ ) -> Generator[None, Any, None]:
41
+ token = implicit_headers_ctx.set(implicit_headers)
42
+ try:
43
+ yield
44
+ finally:
45
+ implicit_headers_ctx.reset(token)
@@ -9,6 +9,7 @@ from aio_pika.abc import AbstractConnection
9
9
  from pydantic import BaseModel
10
10
 
11
11
  from jararaca.broker_backend import MessageBrokerBackend
12
+ from jararaca.messagebus import implicit_headers
12
13
  from jararaca.messagebus.interceptors.publisher_interceptor import (
13
14
  MessageBusConnectionFactory,
14
15
  )
@@ -42,8 +43,12 @@ class AIOPikaMessagePublisher(MessagePublisher):
42
43
  logging.warning(f"Exchange {self.exchange_name} not found")
43
44
  return
44
45
  routing_key = f"{topic}."
46
+
47
+ implicit_headers_data = implicit_headers.use_implicit_headers()
45
48
  await exchange.publish(
46
- aio_pika.Message(body=message.model_dump_json().encode()),
49
+ aio_pika.Message(
50
+ body=message.model_dump_json().encode(), headers=implicit_headers_data
51
+ ),
47
52
  routing_key=routing_key,
48
53
  )
49
54
 
@@ -50,6 +50,7 @@ from jararaca.messagebus.decorators import (
50
50
  MessageHandlerData,
51
51
  ScheduleDispatchData,
52
52
  )
53
+ from jararaca.messagebus.implicit_headers import provide_implicit_headers
53
54
  from jararaca.messagebus.message import Message, MessageOf
54
55
  from jararaca.microservice import (
55
56
  AppTransactionContext,
@@ -1130,6 +1131,8 @@ class ScheduledMessageHandlerCallback:
1130
1131
  AppTransactionContext(
1131
1132
  controller_member_reflect=scheduled_action.controller_member,
1132
1133
  transaction_data=SchedulerTransactionData(
1134
+ task_name=scheduled_action.spec.name
1135
+ or scheduled_action.callable.__qualname__,
1133
1136
  scheduled_to=datetime.now(UTC),
1134
1137
  cron_expression=scheduled_action.spec.cron,
1135
1138
  triggered_at=datetime.now(UTC),
@@ -1469,7 +1472,9 @@ class MessageHandlerCallback:
1469
1472
  incoming_message_spec = MessageHandler.get_message_incoming(handler)
1470
1473
  assert incoming_message_spec is not None
1471
1474
 
1472
- with provide_shutdown_state(self.consumer.shutdown_state):
1475
+ with provide_implicit_headers(aio_pika_message.headers), provide_shutdown_state(
1476
+ self.consumer.shutdown_state
1477
+ ):
1473
1478
  async with self.consumer.uow_context_provider(
1474
1479
  AppTransactionContext(
1475
1480
  controller_member_reflect=handler_data.controller_member,
jararaca/microservice.py CHANGED
@@ -19,7 +19,7 @@ from typing import (
19
19
  runtime_checkable,
20
20
  )
21
21
 
22
- from fastapi import Request, WebSocket
22
+ from fastapi import Request, Response, WebSocket
23
23
 
24
24
  from jararaca.core.providers import ProviderSpec, T, Token
25
25
  from jararaca.messagebus import MessageOf
@@ -34,6 +34,7 @@ if TYPE_CHECKING:
34
34
 
35
35
  @dataclass
36
36
  class SchedulerTransactionData:
37
+ task_name: str
37
38
  triggered_at: datetime
38
39
  scheduled_to: datetime
39
40
  cron_expression: str
@@ -43,6 +44,7 @@ class SchedulerTransactionData:
43
44
  @dataclass
44
45
  class HttpTransactionData:
45
46
  request: Request
47
+ response: Response
46
48
  context_type: Literal["http"] = "http"
47
49
 
48
50
 
@@ -1,3 +1,4 @@
1
+ import inspect
1
2
  from contextlib import contextmanager, suppress
2
3
  from contextvars import ContextVar
3
4
  from functools import wraps
@@ -8,21 +9,35 @@ from typing import (
8
9
  Callable,
9
10
  ContextManager,
10
11
  Generator,
11
- ParamSpec,
12
+ Mapping,
12
13
  Protocol,
14
+ Sequence,
13
15
  TypeVar,
16
+ Union,
14
17
  )
15
18
 
16
19
  from jararaca.microservice import AppTransactionContext
17
20
 
18
- P = ParamSpec("P")
19
- R = TypeVar("R")
21
+ F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
22
+
23
+ AttributeValue = Union[
24
+ str,
25
+ bool,
26
+ int,
27
+ float,
28
+ Sequence[str],
29
+ Sequence[bool],
30
+ Sequence[int],
31
+ Sequence[float],
32
+ ]
33
+
34
+ AttributeMap = Mapping[str, AttributeValue]
20
35
 
21
36
 
22
37
  class TracingContextProvider(Protocol):
23
38
 
24
39
  def __call__(
25
- self, trace_name: str, context_attributes: dict[str, str]
40
+ self, trace_name: str, context_attributes: AttributeMap | None
26
41
  ) -> ContextManager[Any]: ...
27
42
 
28
43
 
@@ -76,22 +91,123 @@ class TracedFunc:
76
91
 
77
92
  def __call__(
78
93
  self,
79
- decorated: Callable[P, Awaitable[R]],
80
- ) -> Callable[P, Awaitable[R]]:
94
+ decorated: F,
95
+ ) -> F:
81
96
 
82
97
  @wraps(decorated)
83
98
  async def wrapper(
84
- *args: P.args,
85
- **kwargs: P.kwargs,
86
- ) -> R:
99
+ *args: Any,
100
+ **kwargs: Any,
101
+ ) -> Any:
87
102
 
88
103
  if ctx_provider := get_tracing_ctx_provider():
89
104
  with ctx_provider(
90
105
  self.trace_name,
91
- self.trace_mapper(**kwargs),
106
+ self.trace_mapper(*args, **kwargs),
92
107
  ):
93
108
  return await decorated(*args, **kwargs)
94
109
 
95
110
  return await decorated(*args, **kwargs)
96
111
 
97
- return wrapper
112
+ return wrapper # type: ignore[return-value]
113
+
114
+
115
+ C = TypeVar("C", bound=type)
116
+
117
+
118
+ class TracedClass:
119
+ """
120
+ Class decorator that automatically applies tracing to all async methods in a class.
121
+
122
+ Usage:
123
+ @TracedClass()
124
+ class MyService:
125
+ async def method1(self) -> str:
126
+ return "hello"
127
+
128
+ async def method2(self, x: int) -> int:
129
+ return x * 2
130
+
131
+ def sync_method(self) -> str: # Not traced
132
+ return "sync"
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ trace_name_prefix: str | None = None,
138
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
139
+ include_private: bool = False,
140
+ exclude_methods: set[str] | None = None,
141
+ ):
142
+ """
143
+ Initialize the TracedClass decorator.
144
+
145
+ Args:
146
+ trace_name_prefix: Prefix for trace names. If None, uses class name.
147
+ trace_mapper: Function to map method arguments to trace attributes.
148
+ include_private: Whether to trace private methods (starting with _).
149
+ exclude_methods: Set of method names to exclude from tracing.
150
+ """
151
+ self.trace_name_prefix = trace_name_prefix
152
+ self.trace_mapper = trace_mapper
153
+ self.include_private = include_private
154
+ self.exclude_methods = exclude_methods or set()
155
+
156
+ def __call__(self, cls: C) -> C:
157
+ """Apply tracing to all async methods in the class."""
158
+
159
+ # Use class name as prefix if not provided
160
+ trace_prefix = self.trace_name_prefix or cls.__name__
161
+
162
+ # Get all methods in the class
163
+ for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
164
+ # Skip if method should be excluded
165
+ if name in self.exclude_methods:
166
+ continue
167
+
168
+ # Skip private methods unless explicitly included
169
+ if name.startswith("_") and not self.include_private:
170
+ continue
171
+
172
+ # Only trace async methods
173
+ if inspect.iscoroutinefunction(method):
174
+ trace_name = f"{trace_prefix}.{name}"
175
+ traced_method = TracedFunc(trace_name, self.trace_mapper)(method)
176
+ setattr(cls, name, traced_method)
177
+
178
+ return cls
179
+
180
+
181
+ def traced_class(
182
+ trace_name_prefix: str | None = None,
183
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
184
+ include_private: bool = False,
185
+ exclude_methods: set[str] | None = None,
186
+ ) -> Callable[[C], C]:
187
+ """
188
+ Functional interface for TracedClass decorator.
189
+
190
+ Usage:
191
+ @traced_class(trace_name_prefix="MyService")
192
+ class MyService:
193
+ async def method1(self) -> str:
194
+ return "hello"
195
+ """
196
+ return TracedClass(
197
+ trace_name_prefix=trace_name_prefix,
198
+ trace_mapper=trace_mapper,
199
+ include_private=include_private,
200
+ exclude_methods=exclude_methods,
201
+ )
202
+
203
+
204
+ __all__ = [
205
+ "TracingContextProvider",
206
+ "TracingContextProviderFactory",
207
+ "provide_tracing_ctx_provider",
208
+ "get_tracing_ctx_provider",
209
+ "default_trace_mapper",
210
+ "TracedFunc",
211
+ "TracedClass",
212
+ "traced_class",
213
+ ]
@@ -0,0 +1,20 @@
1
+ from contextlib import contextmanager
2
+ from typing import Any, Generator
3
+
4
+ from jararaca.observability.decorators import AttributeMap, get_tracing_ctx_provider
5
+
6
+
7
+ @contextmanager
8
+ def spawn_trace(
9
+ name: str,
10
+ attributes: AttributeMap | None = None,
11
+ ) -> Generator[None, Any, None]:
12
+
13
+ if trace_context_provider := get_tracing_ctx_provider():
14
+ with trace_context_provider(trace_name=name, context_attributes=attributes):
15
+ yield
16
+ else:
17
+ yield
18
+
19
+
20
+ spawn_trace
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from contextlib import asynccontextmanager, contextmanager
3
- from typing import AsyncGenerator, Generator, Protocol
3
+ from typing import Any, AsyncGenerator, Generator, Protocol
4
4
 
5
5
  from opentelemetry import metrics, trace
6
6
  from opentelemetry._logs import set_logger_provider
@@ -23,11 +23,21 @@ 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 AppTransactionContext, Container, Microservice
26
+ from jararaca.messagebus.implicit_headers import (
27
+ ImplicitHeaders,
28
+ provide_implicit_headers,
29
+ use_implicit_headers,
30
+ )
31
+ from jararaca.microservice import (
32
+ AppTransactionContext,
33
+ Container,
34
+ Microservice,
35
+ use_app_transaction_context,
36
+ )
27
37
  from jararaca.observability.decorators import (
38
+ AttributeMap,
28
39
  TracingContextProvider,
29
40
  TracingContextProviderFactory,
30
- get_tracing_ctx_provider,
31
41
  )
32
42
  from jararaca.observability.interceptor import ObservabilityProvider
33
43
 
@@ -43,7 +53,7 @@ class OtelTracingContextProvider(TracingContextProvider):
43
53
  def __call__(
44
54
  self,
45
55
  trace_name: str,
46
- context_attributes: dict[str, str],
56
+ context_attributes: AttributeMap | None,
47
57
  ) -> Generator[None, None, None]:
48
58
 
49
59
  with tracer.start_as_current_span(trace_name, attributes=context_attributes):
@@ -63,7 +73,7 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
63
73
  ) -> AsyncGenerator[None, None]:
64
74
 
65
75
  title: str = "Unmapped App Context Execution"
66
- headers = {}
76
+ headers: dict[str, Any] = {}
67
77
  tx_data = app_tx_ctx.transaction_data
68
78
  if tx_data.context_type == "http":
69
79
 
@@ -72,6 +82,19 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
72
82
 
73
83
  elif tx_data.context_type == "message_bus":
74
84
  title = f"Message Bus {tx_data.topic}"
85
+ headers = use_implicit_headers() or {}
86
+
87
+ elif tx_data.context_type == "websocket":
88
+ headers = dict(tx_data.websocket.headers)
89
+ title = f"WebSocket {tx_data.websocket.url}"
90
+
91
+ elif tx_data.context_type == "scheduler":
92
+ title = f"Scheduler Task {tx_data.task_name}"
93
+ headers = {
94
+ "scheduled_to": tx_data.scheduled_to.isoformat(),
95
+ "cron_expression": tx_data.cron_expression,
96
+ "triggered_at": tx_data.triggered_at.isoformat(),
97
+ }
75
98
 
76
99
  carrier = {
77
100
  key: value
@@ -90,8 +113,23 @@ class OtelTracingContextProviderFactory(TracingContextProviderFactory):
90
113
 
91
114
  ctx2 = W3CBaggagePropagator().extract(b2, context=ctx)
92
115
 
93
- with tracer.start_as_current_span(name=title, context=ctx2):
94
- yield
116
+ with tracer.start_as_current_span(
117
+ name=title,
118
+ context=ctx2,
119
+ attributes={
120
+ "app.context_type": tx_data.context_type,
121
+ },
122
+ ) as root_span:
123
+ cx = root_span.get_span_context()
124
+ if app_tx_ctx.transaction_data.context_type == "http":
125
+ app_tx_ctx.transaction_data.response.headers["traceparent"] = hex(
126
+ cx.trace_id
127
+ )[2:].rjust(32, "0")
128
+ tracing_headers: ImplicitHeaders = {}
129
+ TraceContextTextMapPropagator().inject(tracing_headers)
130
+ W3CBaggagePropagator().inject(tracing_headers)
131
+ with provide_implicit_headers(tracing_headers):
132
+ yield
95
133
 
96
134
 
97
135
  class LoggerHandlerCallback(Protocol):
@@ -99,6 +137,21 @@ class LoggerHandlerCallback(Protocol):
99
137
  def __call__(self, logger_handler: logging.Handler) -> None: ...
100
138
 
101
139
 
140
+ class CustomLoggingHandler(LoggingHandler):
141
+
142
+ def _translate(self, record: logging.LogRecord) -> dict[str, Any]:
143
+ try:
144
+ ctx = use_app_transaction_context()
145
+ return {
146
+ **super()._translate(record),
147
+ "attributes": {
148
+ "context_type": ctx.transaction_data.context_type,
149
+ },
150
+ }
151
+ except LookupError:
152
+ return super()._translate(record)
153
+
154
+
102
155
  class OtelObservabilityProvider(ObservabilityProvider):
103
156
 
104
157
  def __init__(
@@ -143,11 +196,11 @@ class OtelObservabilityProvider(ObservabilityProvider):
143
196
  BatchLogRecordProcessor(self.logs_exporter)
144
197
  )
145
198
 
146
- logging_handler = LoggingHandler(
199
+ logging_handler = CustomLoggingHandler(
147
200
  level=logging.DEBUG, logger_provider=logger_provider
148
201
  )
149
202
 
150
- logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
203
+ # logging_handler.addFilter(lambda _: get_tracing_ctx_provider() is not None)
151
204
 
152
205
  self.logging_handler_callback(logging_handler)
153
206
 
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  import os
2
3
  import signal
3
4
  import threading
@@ -5,7 +6,7 @@ from contextlib import asynccontextmanager
5
6
  from signal import SIGINT, SIGTERM
6
7
  from typing import Any, AsyncGenerator
7
8
 
8
- from fastapi import Depends, FastAPI, Request, WebSocket
9
+ from fastapi import Depends, FastAPI, HTTPException, Request, Response, WebSocket
9
10
  from starlette.types import ASGIApp
10
11
 
11
12
  from jararaca.core.uow import UnitOfWorkContextProvider
@@ -23,6 +24,8 @@ from jararaca.presentation.decorators import RestController
23
24
  from jararaca.presentation.http_microservice import HttpMicroservice
24
25
  from jararaca.reflect.controller_inspect import ControllerMemberReflect
25
26
 
27
+ logger = logging.getLogger(__name__)
28
+
26
29
 
27
30
  class HttpAppLifecycle:
28
31
 
@@ -109,7 +112,7 @@ class HttpUowContextProviderDependency:
109
112
  self.shutdown_state.setup_signal_handlers()
110
113
 
111
114
  async def __call__(
112
- self, websocket: WebSocket = None, request: Request = None # type: ignore
115
+ self, websocket: WebSocket = None, request: Request = None, response: Response = None # type: ignore
113
116
  ) -> AsyncGenerator[None, None]:
114
117
  if request:
115
118
  endpoint = request.scope["endpoint"]
@@ -133,13 +136,34 @@ class HttpUowContextProviderDependency:
133
136
  AppTransactionContext(
134
137
  controller_member_reflect=member,
135
138
  transaction_data=(
136
- HttpTransactionData(request=request)
139
+ HttpTransactionData(request=request, response=response)
137
140
  if request
138
141
  else WebSocketTransactionData(websocket=websocket)
139
142
  ),
140
143
  )
141
144
  ):
142
- yield
145
+ try:
146
+ yield
147
+ except HTTPException:
148
+ raise
149
+ except Exception as e:
150
+ logger.exception("Unhandled exception in request handling.")
151
+ raise HTTPException(
152
+ status_code=500,
153
+ detail={
154
+ "message": "Internal server error occurred.",
155
+ "x-traceparentid": (
156
+ response.headers.get("traceparent")
157
+ if response
158
+ else None
159
+ ),
160
+ },
161
+ headers=(
162
+ {k: str(v) for k, v in response.headers.items()}
163
+ if response
164
+ else {}
165
+ ),
166
+ ) from e
143
167
 
144
168
 
145
169
  def create_http_server(
@@ -59,6 +59,7 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
59
59
  consume_broadcast_timeout: int = 1,
60
60
  consume_send_timeout: int = 1,
61
61
  retry_delay: float = 5.0,
62
+ max_concurrent_tasks: int = 1000,
62
63
  ) -> None:
63
64
 
64
65
  self.redis = conn
@@ -67,6 +68,8 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
67
68
 
68
69
  self.lock = asyncio.Lock()
69
70
  self.tasks: set[asyncio.Task[Any]] = set()
71
+ self.max_concurrent_tasks = max_concurrent_tasks
72
+ self.task_semaphore = asyncio.Semaphore(max_concurrent_tasks)
70
73
 
71
74
  self.consume_broadcast_timeout = consume_broadcast_timeout
72
75
  self.consume_send_timeout = consume_send_timeout
@@ -101,16 +104,26 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
101
104
  return self.__broadcast_func
102
105
 
103
106
  async def broadcast(self, message: bytes) -> None:
104
- await self.redis.publish(
105
- self.broadcast_pubsub_channel,
106
- BroadcastMessage.from_message(message).encode(),
107
- )
107
+ try:
108
+ await self.redis.publish(
109
+ self.broadcast_pubsub_channel,
110
+ BroadcastMessage.from_message(message).encode(),
111
+ )
112
+ except Exception as e:
113
+ logger.error(
114
+ f"Failed to publish broadcast message to Redis: {e}", exc_info=True
115
+ )
116
+ raise
108
117
 
109
118
  async def send(self, rooms: list[str], message: bytes) -> None:
110
- await self.redis.publish(
111
- self.send_pubsub_channel,
112
- SendToRoomsMessage.from_message(rooms, message).encode(),
113
- )
119
+ try:
120
+ await self.redis.publish(
121
+ self.send_pubsub_channel,
122
+ SendToRoomsMessage.from_message(rooms, message).encode(),
123
+ )
124
+ except Exception as e:
125
+ logger.error(f"Failed to publish send message to Redis: {e}", exc_info=True)
126
+ raise
114
127
 
115
128
  def configure(
116
129
  self, broadcast: BroadcastFunc, send: SendFunc, shutdown_event: asyncio.Event
@@ -129,7 +142,12 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
129
142
  self.consume_send(self.send_func, self.shutdown_event)
130
143
  )
131
144
 
132
- self.tasks.add(send_task)
145
+ # Use lock when modifying tasks set to prevent race conditions
146
+ async def add_task() -> None:
147
+ async with self.lock:
148
+ self.tasks.add(send_task)
149
+
150
+ asyncio.get_event_loop().create_task(add_task())
133
151
  send_task.add_done_callback(self.handle_send_task_done)
134
152
 
135
153
  def setup_broadcast_consumer(self) -> None:
@@ -138,11 +156,23 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
138
156
  self.consume_broadcast(self.broadcast_func, self.shutdown_event)
139
157
  )
140
158
 
141
- self.tasks.add(broadcast_task)
159
+ # Use lock when modifying tasks set to prevent race conditions
160
+ async def add_task() -> None:
161
+ async with self.lock:
162
+ self.tasks.add(broadcast_task)
163
+
164
+ asyncio.get_event_loop().create_task(add_task())
142
165
 
143
166
  broadcast_task.add_done_callback(self.handle_broadcast_task_done)
144
167
 
145
168
  def handle_broadcast_task_done(self, task: asyncio.Task[Any]) -> None:
169
+ # Remove task from set safely with lock
170
+ async def remove_task() -> None:
171
+ async with self.lock:
172
+ self.tasks.discard(task)
173
+
174
+ asyncio.get_event_loop().create_task(remove_task())
175
+
146
176
  if task.cancelled():
147
177
  logger.warning("Broadcast task was cancelled.")
148
178
  elif task.exception() is not None:
@@ -162,6 +192,13 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
162
192
  )
163
193
 
164
194
  def handle_send_task_done(self, task: asyncio.Task[Any]) -> None:
195
+ # Remove task from set safely with lock
196
+ async def remove_task() -> None:
197
+ async with self.lock:
198
+ self.tasks.discard(task)
199
+
200
+ asyncio.get_event_loop().create_task(remove_task())
201
+
165
202
  if task.cancelled():
166
203
  logger.warning("Send task was cancelled.")
167
204
  elif task.exception() is not None:
@@ -204,54 +241,132 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
204
241
  self, broadcast: BroadcastFunc, shutdown_event: asyncio.Event
205
242
  ) -> None:
206
243
  logger.info("Starting broadcast consumer...")
207
- async with self.redis.pubsub() as pubsub:
208
- await pubsub.subscribe(self.broadcast_pubsub_channel)
209
-
210
- while not shutdown_event.is_set():
211
- message: dict[str, Any] | None = await pubsub.get_message(
212
- ignore_subscribe_messages=True,
213
- timeout=self.consume_broadcast_timeout,
244
+ try:
245
+ # Validate Redis connection before starting
246
+ try:
247
+ await self.redis.ping()
248
+ logger.info("Redis connection validated for broadcast consumer")
249
+ except Exception as e:
250
+ logger.error(f"Redis connection validation failed: {e}", exc_info=True)
251
+ raise
252
+
253
+ async with self.redis.pubsub() as pubsub:
254
+ await pubsub.subscribe(self.broadcast_pubsub_channel)
255
+ logger.info(
256
+ f"Subscribed to broadcast channel: {self.broadcast_pubsub_channel}"
214
257
  )
215
258
 
216
- if message is None:
217
- continue
218
-
219
- broadcast_message = BroadcastMessage.decode(message["data"])
220
-
221
- async with self.lock:
222
- task = asyncio.get_event_loop().create_task(
223
- broadcast(message=broadcast_message.message)
259
+ while not shutdown_event.is_set():
260
+ message: dict[str, Any] | None = await pubsub.get_message(
261
+ ignore_subscribe_messages=True,
262
+ timeout=self.consume_broadcast_timeout,
224
263
  )
225
264
 
226
- self.tasks.add(task)
227
-
228
- task.add_done_callback(self.tasks.discard)
265
+ if message is None:
266
+ continue
267
+
268
+ broadcast_message = BroadcastMessage.decode(message["data"])
269
+
270
+ # Use semaphore for backpressure control
271
+ acquired = False
272
+ try:
273
+ await self.task_semaphore.acquire()
274
+ acquired = True
275
+
276
+ async def broadcast_with_cleanup(msg: bytes) -> None:
277
+ try:
278
+ await broadcast(message=msg)
279
+ finally:
280
+ self.task_semaphore.release()
281
+
282
+ async with self.lock:
283
+ task = asyncio.get_event_loop().create_task(
284
+ broadcast_with_cleanup(broadcast_message.message)
285
+ )
286
+
287
+ self.tasks.add(task)
288
+
289
+ task.add_done_callback(self.tasks.discard)
290
+ except Exception as e:
291
+ # Release semaphore if we acquired it but failed to create task
292
+ if acquired:
293
+ self.task_semaphore.release()
294
+ logger.error(
295
+ f"Error processing broadcast message: {e}", exc_info=True
296
+ )
297
+ # Continue processing other messages
298
+ continue
299
+ except Exception as e:
300
+ logger.error(
301
+ f"Fatal error in broadcast consumer, will retry: {e}", exc_info=True
302
+ )
303
+ raise
229
304
 
230
305
  async def consume_send(self, send: SendFunc, shutdown_event: asyncio.Event) -> None:
231
306
  logger.info("Starting send consumer...")
232
- async with self.redis.pubsub() as pubsub:
233
- await pubsub.subscribe(self.send_pubsub_channel)
234
-
235
- while not shutdown_event.is_set():
236
-
237
- message: dict[str, Any] | None = await pubsub.get_message(
238
- ignore_subscribe_messages=True, timeout=self.consume_send_timeout
239
- )
240
-
241
- if message is None:
242
- continue
243
-
244
- send_message = SendToRoomsMessage.decode(message["data"])
245
-
246
- async with self.lock:
247
-
248
- task = asyncio.get_event_loop().create_task(
249
- send(send_message.rooms, send_message.message)
307
+ try:
308
+ # Validate Redis connection before starting
309
+ try:
310
+ await self.redis.ping()
311
+ logger.info("Redis connection validated for send consumer")
312
+ except Exception as e:
313
+ logger.error(f"Redis connection validation failed: {e}", exc_info=True)
314
+ raise
315
+
316
+ async with self.redis.pubsub() as pubsub:
317
+ await pubsub.subscribe(self.send_pubsub_channel)
318
+ logger.info(f"Subscribed to send channel: {self.send_pubsub_channel}")
319
+
320
+ while not shutdown_event.is_set():
321
+ message: dict[str, Any] | None = await pubsub.get_message(
322
+ ignore_subscribe_messages=True,
323
+ timeout=self.consume_send_timeout,
250
324
  )
251
325
 
252
- self.tasks.add(task)
253
-
254
- task.add_done_callback(self.tasks.discard)
326
+ if message is None:
327
+ continue
328
+
329
+ send_message = SendToRoomsMessage.decode(message["data"])
330
+
331
+ # Use semaphore for backpressure control
332
+ acquired = False
333
+ try:
334
+ await self.task_semaphore.acquire()
335
+ acquired = True
336
+
337
+ async def send_with_cleanup(
338
+ rooms: list[str], msg: bytes
339
+ ) -> None:
340
+ try:
341
+ await send(rooms, msg)
342
+ finally:
343
+ self.task_semaphore.release()
344
+
345
+ async with self.lock:
346
+
347
+ task = asyncio.get_event_loop().create_task(
348
+ send_with_cleanup(
349
+ send_message.rooms, send_message.message
350
+ )
351
+ )
352
+
353
+ self.tasks.add(task)
354
+
355
+ task.add_done_callback(self.tasks.discard)
356
+ except Exception as e:
357
+ # Release semaphore if we acquired it but failed to create task
358
+ if acquired:
359
+ self.task_semaphore.release()
360
+ logger.error(
361
+ f"Error processing send message: {e}", exc_info=True
362
+ )
363
+ # Continue processing other messages
364
+ continue
365
+ except Exception as e:
366
+ logger.error(
367
+ f"Fatal error in send consumer, will retry: {e}", exc_info=True
368
+ )
369
+ raise
255
370
 
256
371
  async def shutdown(self) -> None:
257
372
  async with self.lock:
@@ -85,13 +85,24 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
85
85
  await self.backend.broadcast(message)
86
86
 
87
87
  async def _broadcast_from_backend(self, message: bytes) -> None:
88
- for websocket in self.all_websockets:
88
+ # Create a copy of the websockets set to avoid modification during iteration
89
+ async with self.lock:
90
+ websockets_to_send = list(self.all_websockets)
91
+
92
+ disconnected_websockets: list[WebSocket] = []
93
+
94
+ for websocket in websockets_to_send:
89
95
  try:
90
96
  if websocket.client_state == WebSocketState.CONNECTED:
91
97
  await websocket.send_bytes(message)
92
98
  except WebSocketDisconnect:
93
- async with self.lock: # TODO: check if this can cause concurrency slowdown issues
94
- self.all_websockets.remove(websocket)
99
+ disconnected_websockets.append(websocket)
100
+
101
+ # Clean up disconnected websockets in a single lock acquisition
102
+ if disconnected_websockets:
103
+ async with self.lock:
104
+ for websocket in disconnected_websockets:
105
+ self.all_websockets.discard(websocket)
95
106
 
96
107
  async def send(self, rooms: list[str], message: WebSocketMessageBase) -> None:
97
108
 
@@ -103,16 +114,28 @@ class WebSocketConnectionManagerImpl(WebSocketConnectionManager):
103
114
  )
104
115
 
105
116
  async def _send_from_backend(self, rooms: list[str], message: bytes) -> None:
117
+ # Create a copy of room memberships to avoid modification during iteration
118
+ async with self.lock:
119
+ room_websockets: dict[str, list[WebSocket]] = {
120
+ room: list(self.rooms.get(room, set())) for room in rooms
121
+ }
122
+
123
+ disconnected_by_room: dict[str, list[WebSocket]] = {room: [] for room in rooms}
124
+
125
+ for room, websockets in room_websockets.items():
126
+ for websocket in websockets:
127
+ try:
128
+ if websocket.client_state == WebSocketState.CONNECTED:
129
+ await websocket.send_bytes(message)
130
+ except WebSocketDisconnect:
131
+ disconnected_by_room[room].append(websocket)
132
+
133
+ # Clean up disconnected websockets in a single lock acquisition
106
134
  async with self.lock:
107
- for room in rooms:
108
- for websocket in self.rooms.get(room, set()):
109
- try:
110
- if websocket.client_state == WebSocketState.CONNECTED:
111
- await websocket.send_bytes(message)
112
- except WebSocketDisconnect:
113
- async with self.lock:
114
- if websocket in self.rooms[room]:
115
- self.rooms[room].remove(websocket)
135
+ for room, disconnected_websockets in disconnected_by_room.items():
136
+ if room in self.rooms:
137
+ for websocket in disconnected_websockets:
138
+ self.rooms[room].discard(websocket)
116
139
 
117
140
  async def join(self, rooms: list[str], websocket: WebSocket) -> None:
118
141
  for room in rooms:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.3.17
3
+ Version: 0.3.19
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -18,11 +18,11 @@ Requires-Dist: croniter (>=3.0.3,<4.0.0)
18
18
  Requires-Dist: fastapi (>=0.113.0,<0.114.0)
19
19
  Requires-Dist: frozendict (>=2.4.6,<3.0.0)
20
20
  Requires-Dist: mako (>=1.3.5,<2.0.0)
21
- Requires-Dist: opentelemetry-api (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
22
- Requires-Dist: opentelemetry-distro (>=0.49b2,<0.50) ; extra == "opentelemetry"
21
+ Requires-Dist: opentelemetry-api (>=1.38.0,<2.0.0) ; extra == "opentelemetry"
22
+ Requires-Dist: opentelemetry-distro (>=0.59b0,<0.60) ; extra == "opentelemetry"
23
23
  Requires-Dist: opentelemetry-exporter-otlp (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
24
24
  Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
25
- Requires-Dist: opentelemetry-sdk (>=1.27.0,<2.0.0) ; extra == "opentelemetry"
25
+ Requires-Dist: opentelemetry-sdk (>=1.38.0,<2.0.0) ; extra == "opentelemetry"
26
26
  Requires-Dist: redis (>=5.0.8,<6.0.0)
27
27
  Requires-Dist: sqlalchemy (>=2.0.34,<3.0.0)
28
28
  Requires-Dist: types-croniter (>=3.0.3.20240731,<4.0.0.0)
@@ -1,7 +1,7 @@
1
1
  LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
2
  README.md,sha256=YmCngjU8llW0l7L3tuXkkfr8qH7V9aBMgfp2jEzeiKg,3517
3
- pyproject.toml,sha256=KJbNO4NulXPNhOpQ6kq0t3H8B6PzFpTOuULyhL_w8zc,2832
4
- jararaca/__init__.py,sha256=IMnvfDoyNWTGVittF_wq2Uxtv_BY_wLN5Om6C3vUsCw,22302
3
+ pyproject.toml,sha256=JdgxP3SwJNa20HtUWhMdpWDnNTFyRb-sCzLStMMDOwM,2739
4
+ jararaca/__init__.py,sha256=niZQiN-Whyw2ExcwQbB8uBDPy90pK36pPwa7dYkecuU,22822
5
5
  jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
6
6
  jararaca/broker_backend/__init__.py,sha256=GzEIuHR1xzgCJD4FE3harNjoaYzxHMHoEL0_clUaC-k,3528
7
7
  jararaca/broker_backend/mapper.py,sha256=vTsi7sWpNvlga1PWPFg0rCJ5joJ0cdzykkIc2Tuvenc,696
@@ -10,7 +10,7 @@ jararaca/cli.py,sha256=n3fTOVSNFNICmbZrLCJNGctpbqLWp39xTSkqlIB6Rds,32005
10
10
  jararaca/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  jararaca/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  jararaca/core/providers.py,sha256=wktH84FK7c1s2wNq-fudf1uMfi3CQBR0neU2czJ_L0U,434
13
- jararaca/core/uow.py,sha256=jKlhU_YsssiN0hC1s056JcNerNagXPcXdZJ5kJYG68c,2252
13
+ jararaca/core/uow.py,sha256=J6shc1TwM7uyRDspekY2EdA8slVDPitvHKg4Bunb48E,3533
14
14
  jararaca/di.py,sha256=h3IsXdYZjJj8PJfnEDn0ZAwdd4EBfh8jU-wWO8ko_t4,76
15
15
  jararaca/files/entity.py.mako,sha256=tjQ-1udAMvVqgRokhsrR4uy3P_OVnbk3XZ8X69ixWhE,3098
16
16
  jararaca/lifecycle.py,sha256=qKlzLQQioS8QkxNJ_FC_5WbmT77cNbc_S7OcQeOoHkI,1895
@@ -18,17 +18,19 @@ jararaca/messagebus/__init__.py,sha256=5jAqPqdcEMYBfQyfZDWPnplYdrfMyJLMcacf3qLyU
18
18
  jararaca/messagebus/bus_message_controller.py,sha256=Xd_qwnX5jUvgBTCarHR36fvtol9lPTsYp2IIGKyQQaE,1487
19
19
  jararaca/messagebus/consumers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  jararaca/messagebus/decorators.py,sha256=71ZvZ5slKruAsTaUHzr2f-D8yaUFl9Xxh9chNLaAs9E,6000
21
+ jararaca/messagebus/implicit_headers.py,sha256=maMvSbWwMklVPW0hX6jZ8gAoKsYXB7saYod85jfoSq4,965
21
22
  jararaca/messagebus/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=_DEHwIH9LYsA26Hu1mo9oHzLZuATgjilU9E3o-ecDjs,6520
23
+ jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py,sha256=vs8kCZYslNwiQTy6wFAzuX_N6Oq9nx35RV0MBWwdM-c,6703
23
24
  jararaca/messagebus/interceptors/publisher_interceptor.py,sha256=ojy1bRhqMgrkQljcGGS8cd8-8pUjL8ZHjIUkdmaAnNM,1325
24
25
  jararaca/messagebus/message.py,sha256=U6cyd2XknX8mtm0333slz5fanky2PFLWCmokAO56vvU,819
25
26
  jararaca/messagebus/publisher.py,sha256=JTkxdKbvxvDWT8nK8PVEyyX061vYYbKQMxRHXrZtcEY,2173
26
- jararaca/messagebus/worker.py,sha256=Feh7b3z68sj7a94z0lwMowLgU41pFF2PGgefhdH8rw8,68926
27
- jararaca/microservice.py,sha256=OYCw5C4797X_tVnM_9sEz8BdjbICPHSVsCixsA_FwE4,11419
28
- jararaca/observability/decorators.py,sha256=MOIr2PttPYYvRwEdfQZEwD5RxKHOTv8UEy9n1YQVoKw,2281
27
+ jararaca/messagebus/worker.py,sha256=vni08oRRB-Vz_qXOOoxRDnBTuO8a_rZ6Zue0qx3UUes,69202
28
+ jararaca/microservice.py,sha256=DW4RVeqgrx4J-dAg17sbzTn_sLXuvV1UOQFde2fpqds,11471
29
+ jararaca/observability/decorators.py,sha256=vKAPGLyKz092rd1K4k3xtI2FrRafN69EEuRZKEhaLSk,5627
30
+ jararaca/observability/hooks.py,sha256=VRYg-vlNlFxu9FqOgxyS9VIY6zDcCcgOuZ0RhTUtjwY,488
29
31
  jararaca/observability/interceptor.py,sha256=U4ZLM0f8j6Q7gMUKKnA85bnvD-Qa0ii79Qa_X8KsXAQ,1498
30
32
  jararaca/observability/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
- jararaca/observability/providers/otel.py,sha256=8N1F32W43t7c8cwmtTh6Yz9b7HyfGFSRVrkMLf3NDXc,6157
33
+ jararaca/observability/providers/otel.py,sha256=eYjejx7SW6FZ9-GTNiAxiTiae9RxQpiUSHiYN1rRZnI,8019
32
34
  jararaca/persistence/base.py,sha256=xnGUbsLNz3gO-9iJt-Sn5NY13Yc9-misP8wLwQuGGoM,1024
33
35
  jararaca/persistence/exports.py,sha256=Ghx4yoFaB4QVTb9WxrFYgmcSATXMNvrOvT8ybPNKXCA,62
34
36
  jararaca/persistence/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -42,14 +44,14 @@ jararaca/presentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
42
44
  jararaca/presentation/decorators.py,sha256=dNcK1xYWhdHVBeWDa8dhqh4z8yQcPkJZTQkfgLXz6RI,11655
43
45
  jararaca/presentation/hooks.py,sha256=WBbU5DG3-MAm2Ro2YraQyYG_HENfizYfyShL2ktHi6k,1980
44
46
  jararaca/presentation/http_microservice.py,sha256=g771JosV6jTY3hQtG-HkLOo-T0e-r3b3rp1ddt99Qf0,533
45
- jararaca/presentation/server.py,sha256=YzTcG39xEmEstrtB6nmVUSf2cMP8cSguppKdz_jA_tE,5537
47
+ jararaca/presentation/server.py,sha256=Kz7H_RuB68b5bphjNlS-7cseH_d-xZMa8V-ETFp_jzI,6563
46
48
  jararaca/presentation/websocket/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
49
  jararaca/presentation/websocket/base_types.py,sha256=AvUeeZ1TFhSiRMcYqZU1HaQNqSrcgTkC5R0ArP5dGmA,146
48
50
  jararaca/presentation/websocket/context.py,sha256=A6K5W3kqo9Hgeh1m6JiI7Cdz5SfbXcaICSVX7u1ARZo,1903
49
51
  jararaca/presentation/websocket/decorators.py,sha256=ZNd5aoA9UkyfHOt1C8D2Ffy2gQUNDEsusVnQuTgExgs,2157
50
- jararaca/presentation/websocket/redis.py,sha256=XG_kfr-msgEdfIZkD5JB_GH5lWoEHXwTzpQNVOONvfc,8985
52
+ jararaca/presentation/websocket/redis.py,sha256=1vykr3mcdSDGpSu1rbb4vGnUZNZEvjRfXlIR7TiSho8,13931
51
53
  jararaca/presentation/websocket/types.py,sha256=M8snAMSdaQlKrwEM2qOgF2qrefo5Meio_oOw620Joc8,308
52
- jararaca/presentation/websocket/websocket_interceptor.py,sha256=JWn_G8Q2WO0-1kmN7-Gv0HkIM6nZ_yjCdGRuXUS8F7A,9191
54
+ jararaca/presentation/websocket/websocket_interceptor.py,sha256=c5q8sBi82jXidK4m9KJo-OXmwb-nKsW-dK1DfRqJnlc,10124
53
55
  jararaca/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
56
  jararaca/reflect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
55
57
  jararaca/reflect/controller_inspect.py,sha256=UtV4pRIOqCoK4ogBTXQE0dyopEQ5LDFhwm-1iJvrkJc,2326
@@ -74,8 +76,8 @@ jararaca/tools/typescript/interface_parser.py,sha256=yOSuOXKOeG0soGFo0fKiZIabu4Y
74
76
  jararaca/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
77
  jararaca/utils/rabbitmq_utils.py,sha256=ytdAFUyv-OBkaVnxezuJaJoLrmN7giZgtKeet_IsMBs,10918
76
78
  jararaca/utils/retry.py,sha256=DzPX_fXUvTqej6BQ8Mt2dvLo9nNlTBm7Kx2pFZ26P2Q,4668
77
- jararaca-0.3.17.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
78
- jararaca-0.3.17.dist-info/METADATA,sha256=PvHosK9SliL8gr08GUz7H9TVNStY1DMWehVN825-LL4,5149
79
- jararaca-0.3.17.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
80
- jararaca-0.3.17.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
81
- jararaca-0.3.17.dist-info/RECORD,,
79
+ jararaca-0.3.19.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
80
+ jararaca-0.3.19.dist-info/METADATA,sha256=Snc9WSwcSrBb-PyTB9v4BWurlSLPhuTaEHjKMZGdUck,5149
81
+ jararaca-0.3.19.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
82
+ jararaca-0.3.19.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
83
+ jararaca-0.3.19.dist-info/RECORD,,
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.3.17"
3
+ version = "0.3.19"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"
@@ -20,9 +20,9 @@ croniter = "^3.0.3"
20
20
  redis = "^5.0.8"
21
21
  websockets = "^13.0.1"
22
22
  opentelemetry-exporter-otlp-proto-http = { version = "^1.27.0", optional = true }
23
- opentelemetry-api = { version = "^1.27.0", optional = true }
24
- opentelemetry-sdk = { version = "^1.27.0", optional = true }
25
- opentelemetry-distro = { version = "^0.49b2", optional = true }
23
+ opentelemetry-api = "^1.38.0"
24
+ opentelemetry-sdk = "^1.38.0"
25
+ opentelemetry-distro = "^0.59b0"
26
26
  opentelemetry-exporter-otlp = { version = "^1.27.0", optional = true }
27
27
  types-croniter = "^3.0.3.20240731"
28
28
  types-redis = "^4.6.0.20240903"