jararaca 0.3.11a16__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.
Files changed (96) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +189 -17
  3. jararaca/__main__.py +4 -0
  4. jararaca/broker_backend/__init__.py +4 -0
  5. jararaca/broker_backend/mapper.py +4 -0
  6. jararaca/broker_backend/redis_broker_backend.py +9 -3
  7. jararaca/cli.py +915 -51
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +8 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/helpers/__init__.py +3 -0
  15. jararaca/helpers/global_scheduler/__init__.py +3 -0
  16. jararaca/helpers/global_scheduler/config.py +21 -0
  17. jararaca/helpers/global_scheduler/controller.py +42 -0
  18. jararaca/helpers/global_scheduler/registry.py +32 -0
  19. jararaca/lifecycle.py +6 -2
  20. jararaca/messagebus/__init__.py +4 -0
  21. jararaca/messagebus/bus_message_controller.py +4 -0
  22. jararaca/messagebus/consumers/__init__.py +3 -0
  23. jararaca/messagebus/decorators.py +121 -61
  24. jararaca/messagebus/implicit_headers.py +49 -0
  25. jararaca/messagebus/interceptors/__init__.py +3 -0
  26. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +62 -11
  27. jararaca/messagebus/interceptors/message_publisher_collector.py +62 -0
  28. jararaca/messagebus/interceptors/publisher_interceptor.py +29 -3
  29. jararaca/messagebus/message.py +4 -0
  30. jararaca/messagebus/publisher.py +6 -0
  31. jararaca/messagebus/worker.py +1002 -459
  32. jararaca/microservice.py +113 -2
  33. jararaca/observability/constants.py +7 -0
  34. jararaca/observability/decorators.py +170 -13
  35. jararaca/observability/fastapi_exception_handler.py +37 -0
  36. jararaca/observability/hooks.py +109 -0
  37. jararaca/observability/interceptor.py +4 -0
  38. jararaca/observability/providers/__init__.py +3 -0
  39. jararaca/observability/providers/otel.py +225 -16
  40. jararaca/persistence/base.py +39 -3
  41. jararaca/persistence/exports.py +4 -0
  42. jararaca/persistence/interceptors/__init__.py +3 -0
  43. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  44. jararaca/persistence/interceptors/constants.py +5 -0
  45. jararaca/persistence/interceptors/decorators.py +50 -0
  46. jararaca/persistence/session.py +3 -0
  47. jararaca/persistence/sort_filter.py +4 -0
  48. jararaca/persistence/utilities.py +73 -20
  49. jararaca/presentation/__init__.py +3 -0
  50. jararaca/presentation/decorators.py +88 -86
  51. jararaca/presentation/exceptions.py +23 -0
  52. jararaca/presentation/hooks.py +4 -0
  53. jararaca/presentation/http_microservice.py +4 -0
  54. jararaca/presentation/server.py +97 -45
  55. jararaca/presentation/websocket/__init__.py +3 -0
  56. jararaca/presentation/websocket/base_types.py +4 -0
  57. jararaca/presentation/websocket/context.py +4 -0
  58. jararaca/presentation/websocket/decorators.py +8 -41
  59. jararaca/presentation/websocket/redis.py +280 -53
  60. jararaca/presentation/websocket/types.py +4 -0
  61. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  62. jararaca/reflect/__init__.py +3 -0
  63. jararaca/reflect/controller_inspect.py +16 -10
  64. jararaca/reflect/decorators.py +252 -0
  65. jararaca/reflect/helpers.py +18 -0
  66. jararaca/reflect/metadata.py +34 -25
  67. jararaca/rpc/__init__.py +3 -0
  68. jararaca/rpc/http/__init__.py +101 -0
  69. jararaca/rpc/http/backends/__init__.py +14 -0
  70. jararaca/rpc/http/backends/httpx.py +43 -9
  71. jararaca/rpc/http/backends/otel.py +4 -0
  72. jararaca/rpc/http/decorators.py +380 -115
  73. jararaca/rpc/http/httpx.py +3 -0
  74. jararaca/scheduler/__init__.py +3 -0
  75. jararaca/scheduler/beat_worker.py +521 -105
  76. jararaca/scheduler/decorators.py +15 -22
  77. jararaca/scheduler/types.py +4 -0
  78. jararaca/tools/app_config/__init__.py +3 -0
  79. jararaca/tools/app_config/decorators.py +7 -19
  80. jararaca/tools/app_config/interceptor.py +6 -2
  81. jararaca/tools/typescript/__init__.py +3 -0
  82. jararaca/tools/typescript/decorators.py +120 -0
  83. jararaca/tools/typescript/interface_parser.py +1077 -174
  84. jararaca/utils/__init__.py +3 -0
  85. jararaca/utils/env_parse_utils.py +133 -0
  86. jararaca/utils/rabbitmq_utils.py +112 -39
  87. jararaca/utils/retry.py +19 -14
  88. jararaca-0.4.0a19.dist-info/LICENSE +674 -0
  89. jararaca-0.4.0a19.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  90. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/METADATA +12 -7
  91. jararaca-0.4.0a19.dist-info/RECORD +96 -0
  92. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/WHEEL +1 -1
  93. pyproject.toml +132 -0
  94. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  95. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  96. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a19.dist-info}/entry_points.txt +0 -0
jararaca/microservice.py CHANGED
@@ -1,3 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import asyncio
1
6
  import inspect
2
7
  import logging
3
8
  from contextlib import contextmanager, suppress
@@ -10,6 +15,7 @@ from typing import (
10
15
  Any,
11
16
  AsyncContextManager,
12
17
  Callable,
18
+ Coroutine,
13
19
  Generator,
14
20
  Literal,
15
21
  Protocol,
@@ -19,7 +25,7 @@ from typing import (
19
25
  runtime_checkable,
20
26
  )
21
27
 
22
- from fastapi import Request, WebSocket
28
+ from fastapi import Request, Response, WebSocket
23
29
 
24
30
  from jararaca.core.providers import ProviderSpec, T, Token
25
31
  from jararaca.messagebus import MessageOf
@@ -34,6 +40,7 @@ if TYPE_CHECKING:
34
40
 
35
41
  @dataclass
36
42
  class SchedulerTransactionData:
43
+ task_name: str
37
44
  triggered_at: datetime
38
45
  scheduled_to: datetime
39
46
  cron_expression: str
@@ -43,6 +50,7 @@ class SchedulerTransactionData:
43
50
  @dataclass
44
51
  class HttpTransactionData:
45
52
  request: Request
53
+ response: Response
46
54
  context_type: Literal["http"] = "http"
47
55
 
48
56
 
@@ -50,6 +58,9 @@ class HttpTransactionData:
50
58
  class MessageBusTransactionData:
51
59
  topic: str
52
60
  message: MessageOf[Message]
61
+ message_type: Type[Message]
62
+ message_id: str | None = field(default=None)
63
+ processing_attempt: int = field(default=0)
53
64
  context_type: Literal["message_bus"] = "message_bus"
54
65
 
55
66
 
@@ -59,6 +70,8 @@ class WebSocketTransactionData:
59
70
  context_type: Literal["websocket"] = "websocket"
60
71
 
61
72
 
73
+ APP_TYPE = Literal["http", "worker", "beat"]
74
+
62
75
  TransactionData = (
63
76
  MessageBusTransactionData
64
77
  | HttpTransactionData
@@ -298,7 +311,7 @@ class Container:
298
311
  self.register(instance, bind_to)
299
312
  return cast(T, instance)
300
313
 
301
- def register(self, instance: T, bind_to: Any) -> None:
314
+ def register(self, instance: Any, bind_to: Any) -> None:
302
315
  self.instances_map[bind_to] = instance
303
316
 
304
317
  def get_by_type(self, token: Type[T]) -> T:
@@ -325,6 +338,104 @@ def provide_container(container: Container) -> Generator[None, None, None]:
325
338
  current_container_ctx.reset(token)
326
339
 
327
340
 
341
+ class ShutdownState(Protocol):
342
+
343
+ def request_shutdown(self) -> None: ...
344
+
345
+ def is_shutdown_requested(self) -> bool: ...
346
+
347
+ async def wait_for_shutdown(self) -> None: ...
348
+
349
+
350
+ shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
351
+
352
+
353
+ def is_shutting_down() -> bool:
354
+ """
355
+ Check if the application is in the process of shutting down.
356
+ """
357
+ return shutdown_state_ctx.get().is_shutdown_requested()
358
+
359
+
360
+ def request_shutdown() -> None:
361
+ """
362
+ Request the application to shut down.
363
+ This will set the shutdown event, allowing the application to gracefully shut down.
364
+ """
365
+ shutdown_state_ctx.get().request_shutdown()
366
+
367
+
368
+ async def wait_for_shutdown() -> None:
369
+ """
370
+ Wait for the shutdown event to be set.
371
+ This function will block until a shutdown is requested.
372
+ """
373
+ await shutdown_state_ctx.get().wait_for_shutdown()
374
+
375
+
376
+ async def shutdown_race(*concurrent_tasks: Coroutine[Any, Any, Any]) -> bool:
377
+ """
378
+ Wait for either a shutdown request or any of the provided tasks to complete.
379
+ This function will return as soon as a shutdown is requested or any task finishes.
380
+ Returns True if shutdown was requested, False if a task completed first.
381
+ """
382
+
383
+ tasks = [asyncio.create_task(t) for t in concurrent_tasks + (wait_for_shutdown(),)]
384
+
385
+ _, pending = await asyncio.wait(
386
+ tasks,
387
+ return_when=asyncio.FIRST_COMPLETED,
388
+ )
389
+
390
+ for task in pending:
391
+ task.cancel()
392
+
393
+ return is_shutting_down()
394
+
395
+
396
+ @contextmanager
397
+ def provide_shutdown_state(
398
+ state: ShutdownState,
399
+ ) -> Generator[None, None, None]:
400
+ """
401
+ Context manager to provide the shutdown state.
402
+ This is used to manage the shutdown event for the application.
403
+ """
404
+
405
+ token = shutdown_state_ctx.set(state)
406
+ try:
407
+ yield
408
+ finally:
409
+ with suppress(ValueError):
410
+ shutdown_state_ctx.reset(token)
411
+
412
+
413
+ app_type_ctx = ContextVar[APP_TYPE]("app_type")
414
+
415
+
416
+ def use_app_type() -> APP_TYPE:
417
+ """
418
+ Returns the current application type.
419
+ This function is used to access the application type in the context of an application transaction.
420
+ If no context is set, it raises a LookupError.
421
+ """
422
+ return app_type_ctx.get()
423
+
424
+
425
+ @contextmanager
426
+ def providing_app_type(app_type: APP_TYPE) -> Generator[None, None, None]:
427
+ """
428
+ Context manager to provide the application type.
429
+ This is used to set the application type for the current transaction.
430
+ """
431
+ token = app_type_ctx.set(app_type)
432
+ try:
433
+ yield
434
+ finally:
435
+ with suppress(ValueError):
436
+ app_type_ctx.reset(token)
437
+
438
+
328
439
  __all__ = [
329
440
  "AppTransactionContext",
330
441
  "AppInterceptor",
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ TRACEPARENT_KEY = "traceparent"
6
+
7
+ __all__ = ["TRACEPARENT_KEY"]
@@ -1,3 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import inspect
1
6
  from contextlib import contextmanager, suppress
2
7
  from contextvars import ContextVar
3
8
  from functools import wraps
@@ -8,23 +13,72 @@ from typing import (
8
13
  Callable,
9
14
  ContextManager,
10
15
  Generator,
11
- ParamSpec,
16
+ Literal,
17
+ Mapping,
12
18
  Protocol,
19
+ Sequence,
13
20
  TypeVar,
21
+ Union,
14
22
  )
15
23
 
16
24
  from jararaca.microservice import AppTransactionContext
17
25
 
18
- P = ParamSpec("P")
19
- R = TypeVar("R")
26
+ F = TypeVar("F", bound=Callable[..., Awaitable[Any]])
27
+
28
+ AttributeValue = Union[
29
+ str,
30
+ bool,
31
+ int,
32
+ float,
33
+ Sequence[str],
34
+ Sequence[bool],
35
+ Sequence[int],
36
+ Sequence[float],
37
+ ]
38
+
39
+ AttributeMap = Mapping[str, AttributeValue]
40
+
41
+
42
+ class TracingSpan(Protocol): ...
43
+
44
+
45
+ class TracingSpanContext(Protocol): ...
20
46
 
21
47
 
22
48
  class TracingContextProvider(Protocol):
23
49
 
24
- def __call__(
25
- self, trace_name: str, context_attributes: dict[str, str]
50
+ def start_span_context(
51
+ self, trace_name: str, context_attributes: AttributeMap | None
26
52
  ) -> ContextManager[Any]: ...
27
53
 
54
+ def add_event(
55
+ self,
56
+ event_name: str,
57
+ event_attributes: AttributeMap | None = None,
58
+ ) -> None: ...
59
+
60
+ def set_span_status(self, status_code: Literal["OK", "ERROR", "UNSET"]) -> None: ...
61
+
62
+ def record_exception(
63
+ self,
64
+ exception: Exception,
65
+ attributes: AttributeMap | None = None,
66
+ escaped: bool = False,
67
+ ) -> None: ...
68
+
69
+ def set_span_attribute(
70
+ self,
71
+ key: str,
72
+ value: AttributeValue,
73
+ ) -> None: ...
74
+
75
+ def update_span_name(self, new_name: str) -> None: ...
76
+
77
+ def add_link(self, span_context: TracingSpanContext) -> None: ...
78
+
79
+ def get_current_span(self) -> TracingSpan | None: ...
80
+ def get_current_span_context(self) -> TracingSpanContext | None: ...
81
+
28
82
 
29
83
  class TracingContextProviderFactory(Protocol):
30
84
 
@@ -76,22 +130,125 @@ class TracedFunc:
76
130
 
77
131
  def __call__(
78
132
  self,
79
- decorated: Callable[P, Awaitable[R]],
80
- ) -> Callable[P, Awaitable[R]]:
133
+ decorated: F,
134
+ ) -> F:
81
135
 
82
136
  @wraps(decorated)
83
137
  async def wrapper(
84
- *args: P.args,
85
- **kwargs: P.kwargs,
86
- ) -> R:
138
+ *args: Any,
139
+ **kwargs: Any,
140
+ ) -> Any:
87
141
 
88
142
  if ctx_provider := get_tracing_ctx_provider():
89
- with ctx_provider(
143
+ with ctx_provider.start_span_context(
90
144
  self.trace_name,
91
- self.trace_mapper(**kwargs),
145
+ self.trace_mapper(*args, **kwargs),
92
146
  ):
93
147
  return await decorated(*args, **kwargs)
94
148
 
95
149
  return await decorated(*args, **kwargs)
96
150
 
97
- return wrapper
151
+ return wrapper # type: ignore[return-value]
152
+
153
+
154
+ C = TypeVar("C", bound=type)
155
+
156
+
157
+ class TracedClass:
158
+ """
159
+ Class decorator that automatically applies tracing to all async methods in a class.
160
+
161
+ Usage:
162
+ @TracedClass()
163
+ class MyService:
164
+ async def method1(self) -> str:
165
+ return "hello"
166
+
167
+ async def method2(self, x: int) -> int:
168
+ return x * 2
169
+
170
+ def sync_method(self) -> str: # Not traced
171
+ return "sync"
172
+ """
173
+
174
+ def __init__(
175
+ self,
176
+ trace_name_prefix: str | None = None,
177
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
178
+ include_private: bool = False,
179
+ exclude_methods: set[str] | None = None,
180
+ ):
181
+ """
182
+ Initialize the TracedClass decorator.
183
+
184
+ Args:
185
+ trace_name_prefix: Prefix for trace names. If None, uses class name.
186
+ trace_mapper: Function to map method arguments to trace attributes.
187
+ include_private: Whether to trace private methods (starting with _).
188
+ exclude_methods: Set of method names to exclude from tracing.
189
+ """
190
+ self.trace_name_prefix = trace_name_prefix
191
+ self.trace_mapper = trace_mapper
192
+ self.include_private = include_private
193
+ self.exclude_methods = exclude_methods or set()
194
+
195
+ def __call__(self, cls: C) -> C:
196
+ """Apply tracing to all async methods in the class."""
197
+
198
+ # Use class name as prefix if not provided
199
+ trace_prefix = self.trace_name_prefix or cls.__name__
200
+
201
+ # Get all methods in the class
202
+ for name, method in inspect.getmembers_static(
203
+ cls, predicate=inspect.isfunction
204
+ ):
205
+ # Skip if method should be excluded
206
+ if name in self.exclude_methods:
207
+ continue
208
+
209
+ # Skip private methods unless explicitly included
210
+ if name.startswith("_") and not self.include_private:
211
+ continue
212
+
213
+ # Only trace async methods
214
+ if inspect.iscoroutinefunction(method):
215
+ trace_name = f"{trace_prefix}.{name}"
216
+ traced_method = TracedFunc(trace_name, self.trace_mapper)(method)
217
+ setattr(cls, name, traced_method)
218
+
219
+ return cls
220
+
221
+
222
+ def traced_class(
223
+ trace_name_prefix: str | None = None,
224
+ trace_mapper: Callable[..., dict[str, str]] = default_trace_mapper,
225
+ include_private: bool = False,
226
+ exclude_methods: set[str] | None = None,
227
+ ) -> Callable[[C], C]:
228
+ """
229
+ Functional interface for TracedClass decorator.
230
+
231
+ Usage:
232
+ @traced_class(trace_name_prefix="MyService")
233
+ class MyService:
234
+ async def method1(self) -> str:
235
+ return "hello"
236
+ """
237
+ return TracedClass(
238
+ trace_name_prefix=trace_name_prefix,
239
+ trace_mapper=trace_mapper,
240
+ include_private=include_private,
241
+ exclude_methods=exclude_methods,
242
+ )
243
+
244
+
245
+ __all__ = [
246
+ "TracingContextProvider",
247
+ "TracingContextProviderFactory",
248
+ "provide_tracing_ctx_provider",
249
+ "get_tracing_ctx_provider",
250
+ "default_trace_mapper",
251
+ "TracedFunc",
252
+ "TracedClass",
253
+ "traced_class",
254
+ ]
@@ -0,0 +1,37 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from fastapi import FastAPI, Request, Response
6
+ from fastapi.exception_handlers import (
7
+ http_exception_handler,
8
+ request_validation_exception_handler,
9
+ )
10
+ from fastapi.exceptions import RequestValidationError
11
+ from fastapi.responses import JSONResponse
12
+ from starlette.exceptions import HTTPException
13
+
14
+ from jararaca.observability.constants import TRACEPARENT_KEY
15
+
16
+
17
+ def setup_fastapi_exception_handler(
18
+ app: FastAPI, trace_header_name: str = "traceparent"
19
+ ) -> None:
20
+ async def base_http_exception_handler(
21
+ request: Request, exc: HTTPException | RequestValidationError
22
+ ) -> JSONResponse | Response:
23
+
24
+ if isinstance(exc, RequestValidationError):
25
+ response = await request_validation_exception_handler(request, exc)
26
+ response.headers[trace_header_name] = request.scope.get(TRACEPARENT_KEY, "")
27
+ return response
28
+ else:
29
+ err_response = await http_exception_handler(request, exc)
30
+
31
+ err_response.headers[trace_header_name] = request.scope.get(
32
+ TRACEPARENT_KEY, ""
33
+ )
34
+ return err_response
35
+
36
+ app.exception_handlers[HTTPException] = base_http_exception_handler
37
+ app.exception_handlers[RequestValidationError] = base_http_exception_handler
@@ -0,0 +1,109 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import logging
6
+ import typing
7
+ from contextlib import contextmanager
8
+ from typing import Any, Generator, Literal
9
+
10
+ from jararaca.observability.decorators import (
11
+ AttributeMap,
12
+ AttributeValue,
13
+ TracingContextProvider,
14
+ TracingSpan,
15
+ TracingSpanContext,
16
+ get_tracing_ctx_provider,
17
+ )
18
+
19
+
20
+ @contextmanager
21
+ def start_span(
22
+ name: str,
23
+ attributes: AttributeMap | None = None,
24
+ ) -> Generator[None, Any, None]:
25
+ if trace_context_provider := get_tracing_ctx_provider():
26
+ with trace_context_provider.start_span_context(
27
+ trace_name=name, context_attributes=attributes
28
+ ):
29
+ yield
30
+ else:
31
+ yield
32
+
33
+
34
+ def spawn_trace(
35
+ name: str,
36
+ attributes: AttributeMap | None = None,
37
+ ) -> typing.ContextManager[None]:
38
+ logging.warning(
39
+ "spawn_trace is deprecated, use start_span as context manager instead."
40
+ )
41
+ return start_span(name=name, attributes=attributes)
42
+
43
+
44
+ def add_event(
45
+ name: str,
46
+ attributes: AttributeMap | None = None,
47
+ ) -> None:
48
+
49
+ if trace_context_provider := get_tracing_ctx_provider():
50
+ trace_context_provider.add_event(
51
+ event_name=name,
52
+ event_attributes=attributes,
53
+ )
54
+
55
+
56
+ def set_span_status(status_code: Literal["OK", "ERROR", "UNSET"]) -> None:
57
+
58
+ if trace_context_provider := get_tracing_ctx_provider():
59
+ trace_context_provider.set_span_status(status_code=status_code)
60
+
61
+
62
+ def record_exception(
63
+ exception: Exception,
64
+ attributes: AttributeMap | None = None,
65
+ escaped: bool = False,
66
+ ) -> None:
67
+
68
+ if trace_context_provider := get_tracing_ctx_provider():
69
+ trace_context_provider.record_exception(
70
+ exception=exception,
71
+ attributes=attributes,
72
+ escaped=escaped,
73
+ )
74
+
75
+
76
+ def set_span_attribute(
77
+ key: str,
78
+ value: AttributeValue,
79
+ ) -> None:
80
+
81
+ if trace_context_provider := get_tracing_ctx_provider():
82
+ trace_context_provider.set_span_attribute(
83
+ key=key,
84
+ value=value,
85
+ )
86
+
87
+
88
+ def get_tracing_provider() -> TracingContextProvider | None:
89
+ return get_tracing_ctx_provider()
90
+
91
+
92
+ def get_current_span_context() -> TracingSpanContext | None:
93
+
94
+ if trace_context_provider := get_tracing_ctx_provider():
95
+ return trace_context_provider.get_current_span_context()
96
+ return None
97
+
98
+
99
+ def get_current_span() -> TracingSpan | None:
100
+
101
+ if trace_context_provider := get_tracing_ctx_provider():
102
+ return trace_context_provider.get_current_span()
103
+ return None
104
+
105
+
106
+ def add_span_link(span_context: TracingSpanContext) -> None:
107
+
108
+ if trace_context_provider := get_tracing_ctx_provider():
109
+ trace_context_provider.add_link(span_context=span_context)
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from contextlib import asynccontextmanager
2
6
  from typing import AsyncContextManager, AsyncGenerator, Protocol
3
7
 
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later