jararaca 0.3.11a16__py3-none-any.whl → 0.4.0a5__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 (88) hide show
  1. README.md +121 -0
  2. jararaca/__init__.py +184 -12
  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 +272 -47
  8. jararaca/common/__init__.py +3 -0
  9. jararaca/core/__init__.py +3 -0
  10. jararaca/core/providers.py +4 -0
  11. jararaca/core/uow.py +41 -7
  12. jararaca/di.py +4 -0
  13. jararaca/files/entity.py.mako +4 -0
  14. jararaca/lifecycle.py +6 -2
  15. jararaca/messagebus/__init__.py +4 -0
  16. jararaca/messagebus/bus_message_controller.py +4 -0
  17. jararaca/messagebus/consumers/__init__.py +3 -0
  18. jararaca/messagebus/decorators.py +33 -67
  19. jararaca/messagebus/implicit_headers.py +49 -0
  20. jararaca/messagebus/interceptors/__init__.py +3 -0
  21. jararaca/messagebus/interceptors/aiopika_publisher_interceptor.py +13 -4
  22. jararaca/messagebus/interceptors/publisher_interceptor.py +4 -0
  23. jararaca/messagebus/message.py +4 -0
  24. jararaca/messagebus/publisher.py +6 -0
  25. jararaca/messagebus/worker.py +850 -383
  26. jararaca/microservice.py +110 -1
  27. jararaca/observability/constants.py +7 -0
  28. jararaca/observability/decorators.py +170 -13
  29. jararaca/observability/fastapi_exception_handler.py +37 -0
  30. jararaca/observability/hooks.py +109 -0
  31. jararaca/observability/interceptor.py +4 -0
  32. jararaca/observability/providers/__init__.py +3 -0
  33. jararaca/observability/providers/otel.py +202 -11
  34. jararaca/persistence/base.py +38 -2
  35. jararaca/persistence/exports.py +4 -0
  36. jararaca/persistence/interceptors/__init__.py +3 -0
  37. jararaca/persistence/interceptors/aiosqa_interceptor.py +86 -73
  38. jararaca/persistence/interceptors/constants.py +5 -0
  39. jararaca/persistence/interceptors/decorators.py +50 -0
  40. jararaca/persistence/session.py +3 -0
  41. jararaca/persistence/sort_filter.py +4 -0
  42. jararaca/persistence/utilities.py +50 -20
  43. jararaca/presentation/__init__.py +3 -0
  44. jararaca/presentation/decorators.py +88 -86
  45. jararaca/presentation/exceptions.py +23 -0
  46. jararaca/presentation/hooks.py +4 -0
  47. jararaca/presentation/http_microservice.py +4 -0
  48. jararaca/presentation/server.py +97 -45
  49. jararaca/presentation/websocket/__init__.py +3 -0
  50. jararaca/presentation/websocket/base_types.py +4 -0
  51. jararaca/presentation/websocket/context.py +4 -0
  52. jararaca/presentation/websocket/decorators.py +8 -41
  53. jararaca/presentation/websocket/redis.py +280 -53
  54. jararaca/presentation/websocket/types.py +4 -0
  55. jararaca/presentation/websocket/websocket_interceptor.py +46 -19
  56. jararaca/reflect/__init__.py +3 -0
  57. jararaca/reflect/controller_inspect.py +16 -10
  58. jararaca/reflect/decorators.py +238 -0
  59. jararaca/reflect/metadata.py +34 -25
  60. jararaca/rpc/__init__.py +3 -0
  61. jararaca/rpc/http/__init__.py +101 -0
  62. jararaca/rpc/http/backends/__init__.py +14 -0
  63. jararaca/rpc/http/backends/httpx.py +43 -9
  64. jararaca/rpc/http/backends/otel.py +4 -0
  65. jararaca/rpc/http/decorators.py +378 -113
  66. jararaca/rpc/http/httpx.py +3 -0
  67. jararaca/scheduler/__init__.py +3 -0
  68. jararaca/scheduler/beat_worker.py +521 -105
  69. jararaca/scheduler/decorators.py +15 -22
  70. jararaca/scheduler/types.py +4 -0
  71. jararaca/tools/app_config/__init__.py +3 -0
  72. jararaca/tools/app_config/decorators.py +7 -19
  73. jararaca/tools/app_config/interceptor.py +6 -2
  74. jararaca/tools/typescript/__init__.py +3 -0
  75. jararaca/tools/typescript/decorators.py +120 -0
  76. jararaca/tools/typescript/interface_parser.py +1074 -173
  77. jararaca/utils/__init__.py +3 -0
  78. jararaca/utils/rabbitmq_utils.py +65 -39
  79. jararaca/utils/retry.py +10 -3
  80. jararaca-0.4.0a5.dist-info/LICENSE +674 -0
  81. jararaca-0.4.0a5.dist-info/LICENSES/GPL-3.0-or-later.txt +232 -0
  82. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/METADATA +11 -7
  83. jararaca-0.4.0a5.dist-info/RECORD +88 -0
  84. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/WHEEL +1 -1
  85. pyproject.toml +131 -0
  86. jararaca-0.3.11a16.dist-info/RECORD +0 -74
  87. /jararaca-0.3.11a16.dist-info/LICENSE → /LICENSE +0 -0
  88. {jararaca-0.3.11a16.dist-info → jararaca-0.4.0a5.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,238 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ from typing import Any, Callable, Self, TypedDict, TypeVar, cast
6
+
7
+ DECORATED_T = TypeVar("DECORATED_T", bound="Callable[..., Any] | type")
8
+
9
+
10
+ S = TypeVar("S", bound="StackableDecorator")
11
+
12
+
13
+ class DecoratorMetadata(TypedDict):
14
+ decorators: "list[StackableDecorator]"
15
+ decorators_by_type: "dict[Any, list[StackableDecorator]]"
16
+
17
+
18
+ class StackableDecorator:
19
+ _ATTR_NAME: str = "__jararaca_stackable_decorator__"
20
+
21
+ def __call__(self, subject: DECORATED_T) -> DECORATED_T:
22
+ self.pre_decorated(subject)
23
+ self.register(subject, self)
24
+ self.post_decorated(subject)
25
+ return subject
26
+
27
+ @classmethod
28
+ def decorator_key(cls) -> Any:
29
+ return cls
30
+
31
+ @classmethod
32
+ def get_or_set_metadata(cls, subject: Any) -> DecoratorMetadata:
33
+ if cls._ATTR_NAME not in subject.__dict__:
34
+ setattr(
35
+ subject,
36
+ cls._ATTR_NAME,
37
+ DecoratorMetadata(decorators=[], decorators_by_type={}),
38
+ )
39
+ return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
40
+
41
+ @classmethod
42
+ def get_metadata(cls, subject: Any) -> DecoratorMetadata | None:
43
+ if hasattr(subject, cls._ATTR_NAME):
44
+ return cast(DecoratorMetadata, getattr(subject, cls._ATTR_NAME))
45
+ return None
46
+
47
+ @classmethod
48
+ def register(cls, subject: Any, decorator: "StackableDecorator") -> None:
49
+ if not cls._ATTR_NAME:
50
+ raise NotImplementedError("Subclasses must define _ATTR_NAME")
51
+
52
+ metadata = cls.get_or_set_metadata(subject)
53
+ metadata["decorators"].append(decorator)
54
+ metadata["decorators_by_type"].setdefault(cls.decorator_key(), []).append(
55
+ decorator
56
+ )
57
+
58
+ @classmethod
59
+ def get(cls, subject: Any) -> list[Self]:
60
+ metadata = cls.get_metadata(subject)
61
+ if metadata is None:
62
+ return []
63
+
64
+ if cls is StackableDecorator:
65
+ return cast(list[Self], metadata["decorators"])
66
+ else:
67
+ return cast(
68
+ list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
69
+ )
70
+
71
+ @classmethod
72
+ def extract_list(cls, subject: Any) -> list[Self]:
73
+ metadata = cls.get_metadata(subject)
74
+ if metadata is None:
75
+ return []
76
+
77
+ if cls is StackableDecorator:
78
+ return cast(list[Self], metadata["decorators"])
79
+ else:
80
+ return cast(
81
+ list[Self], metadata["decorators_by_type"].get(cls.decorator_key(), [])
82
+ )
83
+
84
+ @classmethod
85
+ def get_fisrt(cls, subject: Any) -> Self | None:
86
+ decorators = cls.get(subject)
87
+ if decorators:
88
+ return decorators[0]
89
+ return None
90
+
91
+ @classmethod
92
+ def get_last(cls, subject: Any) -> Self | None:
93
+ decorators = cls.get(subject)
94
+ if decorators:
95
+ return decorators[-1]
96
+ return None
97
+
98
+ def pre_decorated(self, subject: DECORATED_T) -> None:
99
+ """
100
+ Hook method called before the subject is decorated.
101
+ Can be overridden by subclasses to perform additional setup.
102
+ """
103
+
104
+ def post_decorated(self, subject: DECORATED_T) -> None:
105
+ """
106
+ Hook method called after the subject has been decorated.
107
+ Can be overridden by subclasses to perform additional setup.
108
+ """
109
+
110
+ @classmethod
111
+ def get_all_from_type(cls, subject_type: type, inherit: bool = True) -> list[Self]:
112
+ """
113
+ Retrieve all decorators of this type from the given class type.
114
+ """
115
+ return resolve_class_decorators(subject_type, cls, inherit)
116
+
117
+ @classmethod
118
+ def get_bound_from_type(
119
+ cls, subject_type: type, inherit: bool = True, last: bool = False
120
+ ) -> Self | None:
121
+ """
122
+ Retrieve the first or last decorator of this type from the given class type.
123
+ """
124
+ return resolve_bound_class_decorators(subject_type, cls, inherit, last=last)
125
+
126
+ @classmethod
127
+ def get_all_from_method(
128
+ cls, cls_subject_type: type, method_name: str, inherit: bool = True
129
+ ) -> list[Self]:
130
+ """
131
+ Retrieve all decorators of this type from the given method.
132
+ """
133
+ return resolve_method_decorators(cls_subject_type, method_name, cls, inherit)
134
+
135
+ @classmethod
136
+ def get_bound_from_method(
137
+ cls,
138
+ cls_subject_type: type,
139
+ method_name: str,
140
+ inherit: bool = True,
141
+ last: bool = True,
142
+ ) -> Self | None:
143
+ """
144
+ Retrieve the first or last decorator of this type from the given method.
145
+ """
146
+ return resolve_bound_method_decorator(
147
+ cls_subject_type, method_name, cls, inherit, last=last
148
+ )
149
+
150
+
151
+ def resolve_class_decorators(
152
+ subject: Any, decorator_cls: type[S], inherit: bool = True
153
+ ) -> list[S]:
154
+ """
155
+ Resolve decorators for a class or instance, optionally inheriting from base classes.
156
+ """
157
+ if not inherit:
158
+ return decorator_cls.get(subject)
159
+
160
+ # If subject is an instance, get its class
161
+ cls = subject if isinstance(subject, type) else type(subject)
162
+
163
+ collected: list[S] = []
164
+ # Iterate MRO in reverse to apply base class decorators first
165
+ for base in reversed(cls.mro()):
166
+ collected.extend(decorator_cls.get(base))
167
+
168
+ return collected
169
+
170
+
171
+ def resolve_bound_class_decorators(
172
+ subject: Any, decorator_cls: type[S], inherit: bool = True, last: bool = False
173
+ ) -> S | None:
174
+ """
175
+ Retrieve the first or last decorator of a given type from a class or instance,
176
+ optionally inheriting from base classes.
177
+ """
178
+ decorators = resolve_class_decorators(subject, decorator_cls, inherit)
179
+ if not decorators:
180
+ return None
181
+ return decorators[-1] if last else decorators[0]
182
+
183
+
184
+ def resolve_method_decorators(
185
+ cls: type,
186
+ method_name: str,
187
+ decorator_cls: type[S],
188
+ inherit: bool = True,
189
+ ) -> list[S]:
190
+ """
191
+ Resolve decorators for a method, optionally inheriting from base classes.
192
+ """
193
+ if not inherit:
194
+ method = getattr(cls, method_name, None)
195
+ if method:
196
+ return decorator_cls.get(method)
197
+ return []
198
+
199
+ collected: list[S] = []
200
+ # Iterate MRO in reverse to apply base class decorators first
201
+ for base in reversed(cls.mro()):
202
+ if method_name in base.__dict__:
203
+ method = base.__dict__[method_name]
204
+ # Handle staticmethod/classmethod wrappers if necessary?
205
+ # Usually decorators are on the underlying function or the wrapper.
206
+ # getattr(cls, name) returns the bound/unbound method.
207
+ # base.__dict__[name] returns the raw object (function or descriptor).
208
+
209
+ # If it's a staticmethod object, it has no __dict__ usually, but we attach attributes to it?
210
+ # The decorator runs on the function BEFORE it becomes a staticmethod.
211
+ # So the attribute should be on the function object.
212
+
213
+ # However, when we access it via base.__dict__[method_name], we might get the staticmethod object.
214
+ # We need to unwrap it if possible.
215
+
216
+ if isinstance(method, (staticmethod, classmethod)):
217
+ method = method.__func__
218
+
219
+ collected.extend(decorator_cls.get(method))
220
+
221
+ return collected
222
+
223
+
224
+ def resolve_bound_method_decorator(
225
+ cls: type,
226
+ method_name: str,
227
+ decorator_cls: type[S],
228
+ inherit: bool = True,
229
+ last: bool = False,
230
+ ) -> S | None:
231
+ """
232
+ Retrieve the first or last decorator of a given type from a method,
233
+ optionally inheriting from base classes.
234
+ """
235
+ decorators = resolve_method_decorators(cls, method_name, decorator_cls, inherit)
236
+ if not decorators:
237
+ return None
238
+ return decorators[-1] if last else decorators[0]
@@ -1,23 +1,32 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from contextlib import contextmanager, suppress
2
6
  from contextvars import ContextVar
3
7
  from dataclasses import dataclass
4
- from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
8
+ from typing import Any, Awaitable, Callable, Generator, Mapping, TypeVar, Union
9
+
10
+ from jararaca.reflect.decorators import StackableDecorator
5
11
 
6
12
  DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
7
13
 
8
14
 
9
- @dataclass
10
- class ControllerInstanceMetadata:
15
+ @dataclass(frozen=True)
16
+ class TransactionMetadata:
11
17
  value: Any
12
- inherited: bool
18
+ """The value of the metadata."""
19
+
20
+ inherited_from_controller: bool
21
+ """Whether the metadata was inherited from a parent class."""
13
22
 
14
23
 
15
- metadata_context: ContextVar[Mapping[str, ControllerInstanceMetadata]] = ContextVar(
24
+ metadata_context: ContextVar[Mapping[str, TransactionMetadata]] = ContextVar(
16
25
  "metadata_context"
17
26
  )
18
27
 
19
28
 
20
- def get_metadata(key: str) -> ControllerInstanceMetadata | None:
29
+ def get_metadata(key: str) -> TransactionMetadata | None:
21
30
  return metadata_context.get({}).get(key)
22
31
 
23
32
 
@@ -28,12 +37,14 @@ def get_metadata_value(key: str, default: Any | None = None) -> Any:
28
37
  return metadata.value
29
38
 
30
39
 
31
- def get_all_metadata() -> Mapping[str, ControllerInstanceMetadata]:
40
+ def get_all_metadata() -> Mapping[str, TransactionMetadata]:
32
41
  return metadata_context.get({})
33
42
 
34
43
 
35
44
  @contextmanager
36
- def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
45
+ def start_transaction_metadata_context(
46
+ metadata: Mapping[str, TransactionMetadata],
47
+ ) -> Generator[None, Any, None]:
37
48
 
38
49
  current_metadata = metadata_context.get({})
39
50
 
@@ -45,23 +56,21 @@ def provide_metadata(metadata: Mapping[str, ControllerInstanceMetadata]) -> Any:
45
56
  metadata_context.reset(token)
46
57
 
47
58
 
48
- class SetMetadata:
59
+ @contextmanager
60
+ def start_providing_metadata(
61
+ **metadata: Any,
62
+ ) -> Generator[None, Any, None]:
63
+
64
+ with start_transaction_metadata_context(
65
+ {
66
+ key: TransactionMetadata(value=value, inherited_from_controller=False)
67
+ for key, value in metadata.items()
68
+ }
69
+ ):
70
+ yield
71
+
72
+
73
+ class SetMetadata(StackableDecorator):
49
74
  def __init__(self, key: str, value: Any) -> None:
50
75
  self.key = key
51
76
  self.value = value
52
-
53
- METATADA_LIST = "__metadata_list__"
54
-
55
- @staticmethod
56
- def register_metadata(cls: DECORATED, value: "SetMetadata") -> None:
57
- metadata_list = getattr(cls, SetMetadata.METATADA_LIST, [])
58
- metadata_list.append(value)
59
- setattr(cls, SetMetadata.METATADA_LIST, metadata_list)
60
-
61
- @staticmethod
62
- def get(cls: DECORATED) -> "list[SetMetadata]":
63
- return cast(list[SetMetadata], getattr(cls, SetMetadata.METATADA_LIST, []))
64
-
65
- def __call__(self, cls: DECORATED) -> DECORATED:
66
- SetMetadata.register_metadata(cls, self)
67
- return cls
jararaca/rpc/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
@@ -0,0 +1,101 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ # HTTP RPC Module - Complete REST Client Implementation
6
+ """
7
+ This module provides a complete REST client implementation with support for:
8
+ - HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
9
+ - Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
10
+ - Configuration decorators (@Timeout, @Retry, @ContentType)
11
+ - Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
12
+ - Caching and response middleware
13
+ - Request/response hooks for customization
14
+ """
15
+
16
+ from .backends.httpx import HTTPXHttpRPCAsyncBackend
17
+ from .decorators import ( # HTTP Method decorators; Request parameter decorators; Configuration decorators; Client builder and core classes; Authentication classes; Middleware and hooks; Configuration classes; Data structures; Error handlers; Exceptions
18
+ ApiKeyAuth,
19
+ AuthenticationMiddleware,
20
+ BasicAuth,
21
+ BearerTokenAuth,
22
+ Body,
23
+ CacheMiddleware,
24
+ ContentType,
25
+ Delete,
26
+ File,
27
+ FormData,
28
+ Get,
29
+ GlobalHttpErrorHandler,
30
+ Header,
31
+ HttpMapping,
32
+ HttpRpcClientBuilder,
33
+ HttpRPCRequest,
34
+ HttpRPCResponse,
35
+ Patch,
36
+ PathParam,
37
+ Post,
38
+ Put,
39
+ Query,
40
+ RequestAttribute,
41
+ RequestHook,
42
+ ResponseHook,
43
+ ResponseMiddleware,
44
+ RestClient,
45
+ Retry,
46
+ RetryConfig,
47
+ RouteHttpErrorHandler,
48
+ RPCRequestNetworkError,
49
+ RPCUnhandleError,
50
+ Timeout,
51
+ TimeoutException,
52
+ )
53
+
54
+ __all__ = [
55
+ # HTTP Method decorators
56
+ "Get",
57
+ "Post",
58
+ "Put",
59
+ "Patch",
60
+ "Delete",
61
+ # Request parameter decorators
62
+ "Query",
63
+ "Header",
64
+ "PathParam",
65
+ "Body",
66
+ "FormData",
67
+ "File",
68
+ # Configuration decorators
69
+ "Timeout",
70
+ "Retry",
71
+ "ContentType",
72
+ # Client builder and core classes
73
+ "RestClient",
74
+ "HttpRpcClientBuilder",
75
+ "HttpMapping",
76
+ "RequestAttribute",
77
+ # Authentication classes
78
+ "BearerTokenAuth",
79
+ "BasicAuth",
80
+ "ApiKeyAuth",
81
+ "AuthenticationMiddleware",
82
+ # Middleware and hooks
83
+ "CacheMiddleware",
84
+ "ResponseMiddleware",
85
+ "RequestHook",
86
+ "ResponseHook",
87
+ # Configuration classes
88
+ "RetryConfig",
89
+ # Data structures
90
+ "HttpRPCRequest",
91
+ "HttpRPCResponse",
92
+ # Error handlers
93
+ "GlobalHttpErrorHandler",
94
+ "RouteHttpErrorHandler",
95
+ # Exceptions
96
+ "RPCRequestNetworkError",
97
+ "RPCUnhandleError",
98
+ "TimeoutException",
99
+ # Backend
100
+ "HTTPXHttpRPCAsyncBackend",
101
+ ]
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ # HTTP RPC Backends
6
+ """
7
+ Backend implementations for HTTP RPC clients.
8
+ """
9
+
10
+ from .httpx import HTTPXHttpRPCAsyncBackend
11
+
12
+ __all__ = [
13
+ "HTTPXHttpRPCAsyncBackend",
14
+ ]
@@ -1,3 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import time
1
6
  from urllib.parse import urljoin
2
7
 
3
8
  import httpx
@@ -7,34 +12,63 @@ from jararaca.rpc.http.decorators import (
7
12
  HttpRPCRequest,
8
13
  HttpRPCResponse,
9
14
  RPCRequestNetworkError,
15
+ TimeoutException,
10
16
  )
11
17
 
12
18
 
13
19
  class HTTPXHttpRPCAsyncBackend(HttpRPCAsyncBackend):
14
20
 
15
- def __init__(self, prefix_url: str = ""):
21
+ def __init__(self, prefix_url: str = "", default_timeout: float = 30.0):
16
22
  self.prefix_url = prefix_url
23
+ self.default_timeout = default_timeout
17
24
 
18
25
  async def request(
19
26
  self,
20
27
  request: HttpRPCRequest,
21
28
  ) -> HttpRPCResponse:
22
29
 
23
- async with httpx.AsyncClient() as client:
30
+ start_time = time.time()
31
+
32
+ # Prepare timeout
33
+ timeout = (
34
+ request.timeout if request.timeout is not None else self.default_timeout
35
+ )
36
+
37
+ # Prepare request kwargs
38
+ request_kwargs = {
39
+ "method": request.method,
40
+ "url": urljoin(self.prefix_url, request.url),
41
+ "headers": request.headers,
42
+ "params": request.query_params,
43
+ "timeout": timeout,
44
+ }
24
45
 
46
+ # Handle different content types
47
+ if request.form_data and request.files:
48
+ # Multipart form data with files
49
+ request_kwargs["data"] = request.form_data
50
+ request_kwargs["files"] = request.files
51
+ elif request.form_data:
52
+ # Form data only
53
+ request_kwargs["data"] = request.form_data
54
+ elif request.body:
55
+ # Raw body content
56
+ request_kwargs["content"] = request.body
57
+
58
+ async with httpx.AsyncClient() as client:
25
59
  try:
26
- response = await client.request(
27
- method=request.method,
28
- url=urljoin(self.prefix_url, request.url),
29
- headers=request.headers,
30
- params=request.query_params,
31
- content=request.body,
32
- )
60
+ response = await client.request(**request_kwargs) # type: ignore[arg-type]
61
+
62
+ elapsed_time = time.time() - start_time
33
63
 
34
64
  return HttpRPCResponse(
35
65
  status_code=response.status_code,
36
66
  data=response.content,
67
+ headers=dict(response.headers),
68
+ elapsed_time=elapsed_time,
37
69
  )
70
+ except httpx.TimeoutException as err:
71
+ raise TimeoutException(f"Request timed out: {err}") from err
38
72
  except httpx.NetworkError as err:
39
73
  raise RPCRequestNetworkError(
40
74
  request=request, backend_request=err.request
@@ -1,3 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Lucas S
2
+ #
3
+ # SPDX-License-Identifier: GPL-3.0-or-later
4
+
1
5
  from opentelemetry import baggage, trace
2
6
  from opentelemetry.baggage.propagation import W3CBaggagePropagator
3
7
  from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator