jararaca 0.3.11a15__py3-none-any.whl → 0.3.12__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/microservice.py CHANGED
@@ -325,6 +325,48 @@ def provide_container(container: Container) -> Generator[None, None, None]:
325
325
  current_container_ctx.reset(token)
326
326
 
327
327
 
328
+ class ShutdownState(Protocol):
329
+
330
+ def request_shutdown(self) -> None: ...
331
+
332
+ def is_shutdown_requested(self) -> bool: ...
333
+
334
+
335
+ shutdown_state_ctx = ContextVar[ShutdownState]("shutdown_state")
336
+
337
+
338
+ def is_shutting_down() -> bool:
339
+ """
340
+ Check if the application is in the process of shutting down.
341
+ """
342
+ return shutdown_state_ctx.get().is_shutdown_requested()
343
+
344
+
345
+ def request_shutdown() -> None:
346
+ """
347
+ Request the application to shut down.
348
+ This will set the shutdown event, allowing the application to gracefully shut down.
349
+ """
350
+ shutdown_state_ctx.get().request_shutdown()
351
+
352
+
353
+ @contextmanager
354
+ def provide_shutdown_state(
355
+ state: ShutdownState,
356
+ ) -> Generator[None, None, None]:
357
+ """
358
+ Context manager to provide the shutdown state.
359
+ This is used to manage the shutdown event for the application.
360
+ """
361
+
362
+ token = shutdown_state_ctx.set(state)
363
+ try:
364
+ yield
365
+ finally:
366
+ with suppress(ValueError):
367
+ shutdown_state_ctx.reset(token)
368
+
369
+
328
370
  __all__ = [
329
371
  "AppTransactionContext",
330
372
  "AppInterceptor",
@@ -1,7 +1,7 @@
1
1
  from contextlib import asynccontextmanager, contextmanager, suppress
2
2
  from contextvars import ContextVar
3
3
  from dataclasses import dataclass
4
- from typing import Any, AsyncGenerator, Generator
4
+ from typing import Any, AsyncGenerator, Generator, Protocol
5
5
 
6
6
  from sqlalchemy.ext.asyncio import (
7
7
  AsyncSession,
@@ -12,9 +12,47 @@ from sqlalchemy.ext.asyncio import (
12
12
  from sqlalchemy.ext.asyncio.engine import AsyncEngine
13
13
 
14
14
  from jararaca.microservice import AppInterceptor, AppTransactionContext
15
- from jararaca.reflect.metadata import SetMetadata, get_metadata_value
15
+ from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
16
+ from jararaca.persistence.interceptors.decorators import (
17
+ INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE,
18
+ )
19
+ from jararaca.reflect.metadata import get_metadata_value
20
+
21
+
22
+ class SessionManager(Protocol):
23
+ def spawn_session(self, connection_name: str | None = None) -> AsyncSession: ...
24
+
25
+
26
+ ctx_session_manager: ContextVar[SessionManager | None] = ContextVar(
27
+ "ctx_session_manager", default=None
28
+ )
29
+
30
+
31
+ @contextmanager
32
+ def providing_session_manager(
33
+ session_manager: SessionManager,
34
+ ) -> Generator[None, Any, None]:
35
+ """
36
+ Context manager to provide a session manager for the current context.
37
+ """
38
+ token = ctx_session_manager.set(session_manager)
39
+ try:
40
+ yield
41
+ finally:
42
+ with suppress(ValueError):
43
+ ctx_session_manager.reset(token)
44
+
45
+
46
+ def use_session_manager() -> SessionManager:
47
+ """
48
+ Retrieve the current session manager from the context variable.
49
+ Raises ValueError if no session manager is set.
50
+ """
51
+ session_manager = ctx_session_manager.get()
52
+ if session_manager is None:
53
+ raise ValueError("No session manager set in the context.")
54
+ return session_manager
16
55
 
17
- DEFAULT_CONNECTION_NAME = "default"
18
56
 
19
57
  ctx_default_connection_name: ContextVar[str] = ContextVar(
20
58
  "ctx_default_connection_name", default=DEFAULT_CONNECTION_NAME
@@ -69,13 +107,21 @@ async def providing_new_session(
69
107
  connection_name: str | None = None,
70
108
  ) -> AsyncGenerator[AsyncSession, None]:
71
109
 
72
- current_session = use_session(connection_name)
110
+ session_manager = use_session_manager()
111
+ current_session = session_manager.spawn_session(connection_name)
73
112
 
74
113
  async with AsyncSession(
75
114
  current_session.bind,
76
115
  ) as new_session, new_session.begin() as new_tx:
77
116
  with providing_session(new_session, new_tx, connection_name):
78
- yield new_session
117
+ try:
118
+ yield new_session
119
+ if new_tx.is_active:
120
+ await new_tx.commit()
121
+ except Exception:
122
+ if new_tx.is_active:
123
+ await new_tx.rollback()
124
+ raise
79
125
 
80
126
 
81
127
  def use_session(connection_name: str | None = None) -> AsyncSession:
@@ -129,50 +175,7 @@ class AIOSQAConfig:
129
175
  self.inject_default = inject_default
130
176
 
131
177
 
132
- INJECT_CONNECTION_METADATA = "inject_connection_metadata_{connection_name}"
133
-
134
-
135
- def set_inject_connection(
136
- inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
137
- ) -> SetMetadata:
138
- """
139
- Set whether to inject the connection metadata for the given connection name.
140
- This is useful when you want to control whether the connection metadata
141
- should be injected into the context or not.
142
- """
143
-
144
- return SetMetadata(
145
- INJECT_CONNECTION_METADATA.format(connection_name=connection_name), inject
146
- )
147
-
148
-
149
- def uses_connection(
150
- connection_name: str = DEFAULT_CONNECTION_NAME,
151
- ) -> SetMetadata:
152
- """
153
- Use connection metadata for the given connection name.
154
- This is useful when you want to inject the connection metadata into the context,
155
- for example, when you are using a specific connection for a specific operation.
156
- """
157
- return SetMetadata(
158
- INJECT_CONNECTION_METADATA.format(connection_name=connection_name), True
159
- )
160
-
161
-
162
- def dnt_uses_connection(
163
- connection_name: str = DEFAULT_CONNECTION_NAME,
164
- ) -> SetMetadata:
165
- """
166
- Do not use connection metadata for the given connection name.
167
- This is useful when you want to ensure that the connection metadata is not injected
168
- into the context, for example, when you are using a different connection for a specific operation.
169
- """
170
- return SetMetadata(
171
- INJECT_CONNECTION_METADATA.format(connection_name=connection_name), False
172
- )
173
-
174
-
175
- class AIOSqlAlchemySessionInterceptor(AppInterceptor):
178
+ class AIOSqlAlchemySessionInterceptor(AppInterceptor, SessionManager):
176
179
 
177
180
  def __init__(self, config: AIOSQAConfig):
178
181
  self.config = config
@@ -189,27 +192,33 @@ class AIOSqlAlchemySessionInterceptor(AppInterceptor):
189
192
  self, app_context: AppTransactionContext
190
193
  ) -> AsyncGenerator[None, None]:
191
194
 
192
- uses_connection_metadata = get_metadata_value(
193
- INJECT_CONNECTION_METADATA.format(
194
- connection_name=self.config.connection_name
195
- ),
196
- self.config.inject_default,
197
- )
198
-
199
- if not uses_connection_metadata:
200
- yield
201
- return
202
-
203
- async with self.sessionmaker() as session, session.begin() as tx:
204
- token = ctx_default_connection_name.set(self.config.connection_name)
205
- with providing_session(session, tx, self.config.connection_name):
206
- try:
207
- yield
208
- if tx.is_active:
209
- await tx.commit()
210
- except Exception as e:
211
- await tx.rollback()
212
- raise e
213
- finally:
214
- with suppress(ValueError):
215
- ctx_default_connection_name.reset(token)
195
+ with providing_session_manager(self):
196
+ uses_connection_metadata = get_metadata_value(
197
+ INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
198
+ connection_name=self.config.connection_name
199
+ ),
200
+ self.config.inject_default,
201
+ )
202
+
203
+ if not uses_connection_metadata:
204
+ yield
205
+ return
206
+
207
+ async with self.sessionmaker() as session, session.begin() as tx:
208
+ token = ctx_default_connection_name.set(self.config.connection_name)
209
+ with providing_session(session, tx, self.config.connection_name):
210
+ try:
211
+ yield
212
+ if tx.is_active:
213
+ await tx.commit()
214
+ except Exception as e:
215
+ await tx.rollback()
216
+ raise e
217
+ finally:
218
+ with suppress(ValueError):
219
+ ctx_default_connection_name.reset(token)
220
+
221
+ def spawn_session(self, connection_name: str | None = None) -> AsyncSession:
222
+ connection_name = ensure_name(connection_name)
223
+ session = self.sessionmaker()
224
+ return session
@@ -0,0 +1 @@
1
+ DEFAULT_CONNECTION_NAME = "default"
@@ -0,0 +1,45 @@
1
+ from jararaca.persistence.interceptors.constants import DEFAULT_CONNECTION_NAME
2
+ from jararaca.reflect.metadata import SetMetadata
3
+
4
+ INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE = (
5
+ "inject_persistence_template_{connection_name}"
6
+ )
7
+
8
+
9
+ def set_use_persistence_session(
10
+ inject: bool, connection_name: str = DEFAULT_CONNECTION_NAME
11
+ ) -> SetMetadata:
12
+ """
13
+ Set whether to inject the connection metadata for the given connection name.
14
+ This is useful when you want to control whether the connection metadata
15
+ should be injected into the context or not.
16
+ """
17
+
18
+ return SetMetadata(
19
+ INJECT_PERSISTENCE_SESSION_METADATA_TEMPLATE.format(
20
+ connection_name=connection_name
21
+ ),
22
+ inject,
23
+ )
24
+
25
+
26
+ def uses_persistence_session(
27
+ connection_name: str = DEFAULT_CONNECTION_NAME,
28
+ ) -> SetMetadata:
29
+ """
30
+ Use connection metadata for the given connection name.
31
+ This is useful when you want to inject the connection metadata into the context,
32
+ for example, when you are using a specific connection for a specific operation.
33
+ """
34
+ return set_use_persistence_session(True, connection_name=connection_name)
35
+
36
+
37
+ def skip_persistence_session(
38
+ connection_name: str = DEFAULT_CONNECTION_NAME,
39
+ ) -> SetMetadata:
40
+ """
41
+ Decorator to skip using connection metadata for the given connection name.
42
+ This is useful when you want to ensure that the connection metadata is not injected
43
+ into the context, for example, when you are using a different connection for a specific operation.
44
+ """
45
+ return set_use_persistence_session(False, connection_name=connection_name)
@@ -1,4 +1,8 @@
1
+ import os
2
+ import signal
3
+ import threading
1
4
  from contextlib import asynccontextmanager
5
+ from signal import SIGINT, SIGTERM
2
6
  from typing import Any, AsyncGenerator
3
7
 
4
8
  from fastapi import Depends, FastAPI, Request, WebSocket
@@ -10,7 +14,9 @@ from jararaca.lifecycle import AppLifecycle
10
14
  from jararaca.microservice import (
11
15
  AppTransactionContext,
12
16
  HttpTransactionData,
17
+ ShutdownState,
13
18
  WebSocketTransactionData,
19
+ provide_shutdown_state,
14
20
  )
15
21
  from jararaca.presentation.decorators import RestController
16
22
  from jararaca.presentation.http_microservice import HttpMicroservice
@@ -76,10 +82,49 @@ class HttpAppLifecycle:
76
82
  yield
77
83
 
78
84
 
85
+ class HttpShutdownState(ShutdownState):
86
+ def __init__(self) -> None:
87
+ self._requested = False
88
+ self.old_signal_handlers = {
89
+ SIGINT: signal.getsignal(SIGINT),
90
+ SIGTERM: signal.getsignal(SIGTERM),
91
+ }
92
+ self.thread_lock = threading.Lock()
93
+
94
+ def request_shutdown(self) -> None:
95
+ if not self._requested:
96
+ self._requested = True
97
+ os.kill(os.getpid(), SIGINT)
98
+
99
+ def is_shutdown_requested(self) -> bool:
100
+ return self._requested
101
+
102
+ def handle_signal(self, signum: int, frame: Any) -> None:
103
+ print(f"Received signal {signum}, initiating shutdown...")
104
+ if self._requested:
105
+ print("Shutdown already requested, ignoring signal.")
106
+ return
107
+ print("Requesting shutdown...")
108
+ self._requested = True
109
+
110
+ # remove the signal handler to prevent recursion
111
+ for sig in (SIGINT, SIGTERM):
112
+ if self.old_signal_handlers[sig] is not None:
113
+ signal.signal(sig, self.old_signal_handlers[sig])
114
+
115
+ signal.raise_signal(signum)
116
+
117
+ def setup_signal_handlers(self) -> None:
118
+ signal.signal(SIGINT, self.handle_signal)
119
+ signal.signal(SIGTERM, self.handle_signal)
120
+
121
+
79
122
  class HttpUowContextProviderDependency:
80
123
 
81
124
  def __init__(self, uow_provider: UnitOfWorkContextProvider) -> None:
82
125
  self.uow_provider = uow_provider
126
+ self.shutdown_state = HttpShutdownState()
127
+ self.shutdown_state.setup_signal_handlers()
83
128
 
84
129
  async def __call__(
85
130
  self, websocket: WebSocket = None, request: Request = None # type: ignore
@@ -101,17 +146,18 @@ class HttpUowContextProviderDependency:
101
146
  "ControllerMemberReflect, but got: {}".format(type(member))
102
147
  )
103
148
 
104
- async with self.uow_provider(
105
- AppTransactionContext(
106
- controller_member_reflect=member,
107
- transaction_data=(
108
- HttpTransactionData(request=request)
109
- if request
110
- else WebSocketTransactionData(websocket=websocket)
111
- ),
112
- )
113
- ):
114
- yield
149
+ with provide_shutdown_state(self.shutdown_state):
150
+ async with self.uow_provider(
151
+ AppTransactionContext(
152
+ controller_member_reflect=member,
153
+ transaction_data=(
154
+ HttpTransactionData(request=request)
155
+ if request
156
+ else WebSocketTransactionData(websocket=websocket)
157
+ ),
158
+ )
159
+ ):
160
+ yield
115
161
 
116
162
 
117
163
  def create_http_server(
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import logging
2
3
  from dataclasses import dataclass
3
4
  from typing import Any
4
5
 
@@ -10,6 +11,8 @@ from jararaca.presentation.websocket.websocket_interceptor import (
10
11
  WebSocketConnectionBackend,
11
12
  )
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
 
14
17
  @dataclass
15
18
  class BroadcastMessage:
@@ -55,6 +58,7 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
55
58
  send_pubsub_channel: str,
56
59
  consume_broadcast_timeout: int = 1,
57
60
  consume_send_timeout: int = 1,
61
+ retry_delay: float = 5.0,
58
62
  ) -> None:
59
63
 
60
64
  self.redis = conn
@@ -66,6 +70,35 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
66
70
 
67
71
  self.consume_broadcast_timeout = consume_broadcast_timeout
68
72
  self.consume_send_timeout = consume_send_timeout
73
+ self.retry_delay = retry_delay
74
+ self.__shutdown_event: asyncio.Event | None = None
75
+
76
+ self.__send_func: SendFunc | None = None
77
+ self.__broadcast_func: BroadcastFunc | None = None
78
+
79
+ @property
80
+ def shutdown_event(self) -> asyncio.Event:
81
+ if self.__shutdown_event is None:
82
+ raise RuntimeError(
83
+ "Shutdown event is not set. Please configure the backend before using it."
84
+ )
85
+ return self.__shutdown_event
86
+
87
+ @property
88
+ def send_func(self) -> SendFunc:
89
+ if self.__send_func is None:
90
+ raise RuntimeError(
91
+ "Send function is not set. Please configure the backend before using it."
92
+ )
93
+ return self.__send_func
94
+
95
+ @property
96
+ def broadcast_func(self) -> BroadcastFunc:
97
+ if self.__broadcast_func is None:
98
+ raise RuntimeError(
99
+ "Broadcast function is not set. Please configure the backend before using it."
100
+ )
101
+ return self.__broadcast_func
69
102
 
70
103
  async def broadcast(self, message: bytes) -> None:
71
104
  await self.redis.publish(
@@ -82,22 +115,95 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
82
115
  def configure(
83
116
  self, broadcast: BroadcastFunc, send: SendFunc, shutdown_event: asyncio.Event
84
117
  ) -> None:
118
+ if self.__shutdown_event is not None:
119
+ raise RuntimeError("Backend is already configured.")
120
+ self.__shutdown_event = shutdown_event
121
+ self.__send_func = send
122
+ self.__broadcast_func = broadcast
123
+ self.setup_send_consumer()
124
+ self.setup_broadcast_consumer()
85
125
 
86
- broadcast_task = asyncio.get_event_loop().create_task(
87
- self.consume_broadcast(broadcast, shutdown_event)
88
- )
126
+ def setup_send_consumer(self) -> None:
89
127
 
90
128
  send_task = asyncio.get_event_loop().create_task(
91
- self.consume_send(send, shutdown_event)
129
+ self.consume_send(self.send_func, self.shutdown_event)
92
130
  )
93
131
 
94
- self.tasks.add(broadcast_task)
95
132
  self.tasks.add(send_task)
133
+ send_task.add_done_callback(self.handle_send_task_done)
134
+
135
+ def setup_broadcast_consumer(self) -> None:
136
+
137
+ broadcast_task = asyncio.get_event_loop().create_task(
138
+ self.consume_broadcast(self.broadcast_func, self.shutdown_event)
139
+ )
140
+
141
+ self.tasks.add(broadcast_task)
142
+
143
+ broadcast_task.add_done_callback(self.handle_broadcast_task_done)
144
+
145
+ def handle_broadcast_task_done(self, task: asyncio.Task[Any]) -> None:
146
+ if task.cancelled():
147
+ logger.warning("Broadcast task was cancelled.")
148
+ elif task.exception() is not None:
149
+ logger.exception(
150
+ f"Broadcast task raised an exception:", exc_info=task.exception()
151
+ )
152
+ else:
153
+ logger.warning("Broadcast task somehow completed successfully.")
154
+
155
+ if not self.shutdown_event.is_set():
156
+ logger.warning(
157
+ "Broadcast task completed, but shutdown event is not set. This is unexpected."
158
+ )
159
+ # Add delay before retrying to avoid excessive CPU usage
160
+ asyncio.get_event_loop().create_task(
161
+ self._retry_broadcast_consumer_with_delay()
162
+ )
163
+
164
+ def handle_send_task_done(self, task: asyncio.Task[Any]) -> None:
165
+ if task.cancelled():
166
+ logger.warning("Send task was cancelled.")
167
+ elif task.exception() is not None:
168
+ logger.exception(
169
+ f"Send task raised an exception:", exc_info=task.exception()
170
+ )
171
+ else:
172
+ logger.warning("Send task somehow completed successfully.")
173
+
174
+ if not self.shutdown_event.is_set():
175
+ logger.warning(
176
+ "Send task completed, but shutdown event is not set. This is unexpected."
177
+ )
178
+ # Add delay before retrying to avoid excessive CPU usage
179
+ asyncio.get_event_loop().create_task(self._retry_send_consumer_with_delay())
180
+
181
+ async def _retry_broadcast_consumer_with_delay(self) -> None:
182
+ """Retry setting up broadcast consumer after a delay to avoid excessive CPU usage."""
183
+ logger.info(
184
+ f"Waiting {self.retry_delay} seconds before retrying broadcast consumer..."
185
+ )
186
+ await asyncio.sleep(self.retry_delay)
187
+
188
+ if not self.shutdown_event.is_set():
189
+ logger.info("Retrying broadcast consumer setup...")
190
+ self.setup_broadcast_consumer()
191
+
192
+ async def _retry_send_consumer_with_delay(self) -> None:
193
+ """Retry setting up send consumer after a delay to avoid excessive CPU usage."""
194
+ logger.info(
195
+ f"Waiting {self.retry_delay} seconds before retrying send consumer..."
196
+ )
197
+ await asyncio.sleep(self.retry_delay)
198
+
199
+ if not self.shutdown_event.is_set():
200
+ logger.info("Retrying send consumer setup...")
201
+ self.setup_send_consumer()
96
202
 
97
203
  async def consume_broadcast(
98
204
  self, broadcast: BroadcastFunc, shutdown_event: asyncio.Event
99
205
  ) -> None:
100
-
206
+ logger.info("Starting broadcast consumer...")
101
207
  async with self.redis.pubsub() as pubsub:
102
208
  await pubsub.subscribe(self.broadcast_pubsub_channel)
103
209
 
@@ -122,7 +228,7 @@ class RedisWebSocketConnectionBackend(WebSocketConnectionBackend):
122
228
  task.add_done_callback(self.tasks.discard)
123
229
 
124
230
  async def consume_send(self, send: SendFunc, shutdown_event: asyncio.Event) -> None:
125
-
231
+ logger.info("Starting send consumer...")
126
232
  async with self.redis.pubsub() as pubsub:
127
233
  await pubsub.subscribe(self.send_pubsub_channel)
128
234
 
@@ -6,7 +6,7 @@ from typing import Any, Awaitable, Callable, Mapping, TypeVar, Union, cast
6
6
  DECORATED = TypeVar("DECORATED", bound=Union[Callable[..., Awaitable[Any]], type])
7
7
 
8
8
 
9
- @dataclass
9
+ @dataclass(frozen=True)
10
10
  class ControllerInstanceMetadata:
11
11
  value: Any
12
12
  inherited: bool
@@ -0,0 +1,97 @@
1
+ # HTTP RPC Module - Complete REST Client Implementation
2
+ """
3
+ This module provides a complete REST client implementation with support for:
4
+ - HTTP method decorators (@Get, @Post, @Put, @Patch, @Delete)
5
+ - Request parameter decorators (@Query, @Header, @PathParam, @Body, @FormData, @File)
6
+ - Configuration decorators (@Timeout, @Retry, @ContentType)
7
+ - Authentication middleware (BearerTokenAuth, BasicAuth, ApiKeyAuth)
8
+ - Caching and response middleware
9
+ - Request/response hooks for customization
10
+ """
11
+
12
+ from .backends.httpx import HTTPXHttpRPCAsyncBackend
13
+ 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
14
+ ApiKeyAuth,
15
+ AuthenticationMiddleware,
16
+ BasicAuth,
17
+ BearerTokenAuth,
18
+ Body,
19
+ CacheMiddleware,
20
+ ContentType,
21
+ Delete,
22
+ File,
23
+ FormData,
24
+ Get,
25
+ GlobalHttpErrorHandler,
26
+ Header,
27
+ HttpMapping,
28
+ HttpRpcClientBuilder,
29
+ HttpRPCRequest,
30
+ HttpRPCResponse,
31
+ Patch,
32
+ PathParam,
33
+ Post,
34
+ Put,
35
+ Query,
36
+ RequestAttribute,
37
+ RequestHook,
38
+ ResponseHook,
39
+ ResponseMiddleware,
40
+ RestClient,
41
+ Retry,
42
+ RetryConfig,
43
+ RouteHttpErrorHandler,
44
+ RPCRequestNetworkError,
45
+ RPCUnhandleError,
46
+ Timeout,
47
+ TimeoutException,
48
+ )
49
+
50
+ __all__ = [
51
+ # HTTP Method decorators
52
+ "Get",
53
+ "Post",
54
+ "Put",
55
+ "Patch",
56
+ "Delete",
57
+ # Request parameter decorators
58
+ "Query",
59
+ "Header",
60
+ "PathParam",
61
+ "Body",
62
+ "FormData",
63
+ "File",
64
+ # Configuration decorators
65
+ "Timeout",
66
+ "Retry",
67
+ "ContentType",
68
+ # Client builder and core classes
69
+ "RestClient",
70
+ "HttpRpcClientBuilder",
71
+ "HttpMapping",
72
+ "RequestAttribute",
73
+ # Authentication classes
74
+ "BearerTokenAuth",
75
+ "BasicAuth",
76
+ "ApiKeyAuth",
77
+ "AuthenticationMiddleware",
78
+ # Middleware and hooks
79
+ "CacheMiddleware",
80
+ "ResponseMiddleware",
81
+ "RequestHook",
82
+ "ResponseHook",
83
+ # Configuration classes
84
+ "RetryConfig",
85
+ # Data structures
86
+ "HttpRPCRequest",
87
+ "HttpRPCResponse",
88
+ # Error handlers
89
+ "GlobalHttpErrorHandler",
90
+ "RouteHttpErrorHandler",
91
+ # Exceptions
92
+ "RPCRequestNetworkError",
93
+ "RPCUnhandleError",
94
+ "TimeoutException",
95
+ # Backend
96
+ "HTTPXHttpRPCAsyncBackend",
97
+ ]
@@ -0,0 +1,10 @@
1
+ # HTTP RPC Backends
2
+ """
3
+ Backend implementations for HTTP RPC clients.
4
+ """
5
+
6
+ from .httpx import HTTPXHttpRPCAsyncBackend
7
+
8
+ __all__ = [
9
+ "HTTPXHttpRPCAsyncBackend",
10
+ ]