airia 0.1.12__py3-none-any.whl → 0.1.14__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.
- airia/client/_request_handler/__init__.py +4 -0
- airia/client/_request_handler/async_request_handler.py +272 -0
- airia/client/_request_handler/base_request_handler.py +108 -0
- airia/client/_request_handler/sync_request_handler.py +255 -0
- airia/client/async_client.py +25 -584
- airia/client/base_client.py +2 -209
- airia/client/conversations/__init__.py +4 -0
- airia/client/conversations/async_conversations.py +187 -0
- airia/client/conversations/base_conversations.py +135 -0
- airia/client/conversations/sync_conversations.py +182 -0
- airia/client/pipeline_execution/__init__.py +4 -0
- airia/client/pipeline_execution/async_pipeline_execution.py +178 -0
- airia/client/pipeline_execution/base_pipeline_execution.py +96 -0
- airia/client/pipeline_execution/sync_pipeline_execution.py +178 -0
- airia/client/pipelines_config/__init__.py +4 -0
- airia/client/pipelines_config/async_pipelines_config.py +127 -0
- airia/client/pipelines_config/base_pipelines_config.py +76 -0
- airia/client/pipelines_config/sync_pipelines_config.py +127 -0
- airia/client/project/__init__.py +4 -0
- airia/client/project/async_project.py +122 -0
- airia/client/project/base_project.py +74 -0
- airia/client/project/sync_project.py +120 -0
- airia/client/store/__init__.py +4 -0
- airia/client/store/async_store.py +377 -0
- airia/client/store/base_store.py +243 -0
- airia/client/store/sync_store.py +352 -0
- airia/client/sync_client.py +25 -563
- airia/constants.py +13 -2
- airia/exceptions.py +8 -8
- airia/logs.py +10 -32
- airia/types/__init__.py +0 -0
- airia/types/_request_data.py +29 -2
- airia/types/api/__init__.py +0 -19
- airia/types/api/conversations/__init__.py +3 -0
- airia/types/api/conversations/_conversations.py +115 -0
- airia/types/api/pipeline_execution/__init__.py +13 -0
- airia/types/api/pipeline_execution/_pipeline_execution.py +76 -0
- airia/types/api/pipelines_config/__init__.py +3 -0
- airia/types/api/pipelines_config/get_pipeline_config.py +401 -0
- airia/types/api/project/__init__.py +3 -0
- airia/types/api/project/get_projects.py +91 -0
- airia/types/api/store/__init__.py +4 -0
- airia/types/api/store/get_file.py +145 -0
- airia/types/api/store/get_files.py +21 -0
- airia/types/sse/__init__.py +8 -0
- airia/types/sse/sse_messages.py +209 -0
- airia/utils/sse_parser.py +40 -7
- airia-0.1.14.dist-info/METADATA +221 -0
- airia-0.1.14.dist-info/RECORD +55 -0
- airia/types/api/conversations.py +0 -14
- airia/types/api/get_pipeline_config.py +0 -183
- airia/types/api/get_projects.py +0 -35
- airia/types/api/pipeline_execution.py +0 -29
- airia-0.1.12.dist-info/METADATA +0 -705
- airia-0.1.12.dist-info/RECORD +0 -23
- {airia-0.1.12.dist-info → airia-0.1.14.dist-info}/WHEEL +0 -0
- {airia-0.1.12.dist-info → airia-0.1.14.dist-info}/licenses/LICENSE +0 -0
- {airia-0.1.12.dist-info → airia-0.1.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import weakref
|
|
3
|
+
from typing import Any, AsyncIterator, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import loguru
|
|
7
|
+
|
|
8
|
+
from ...exceptions import AiriaAPIError
|
|
9
|
+
from ...types._request_data import RequestData
|
|
10
|
+
from ...utils.sse_parser import async_parse_sse_stream_chunked
|
|
11
|
+
from .base_request_handler import BaseRequestHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AsyncRequestHandler(BaseRequestHandler):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
logger: "loguru.Logger",
|
|
18
|
+
timeout: float,
|
|
19
|
+
base_url: str,
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
bearer_token: Optional[str] = None,
|
|
22
|
+
log_requests: bool = False,
|
|
23
|
+
):
|
|
24
|
+
self.session = aiohttp.ClientSession()
|
|
25
|
+
|
|
26
|
+
self._finalizer = weakref.finalize(self, self._cleanup_session, self.session)
|
|
27
|
+
|
|
28
|
+
super().__init__(
|
|
29
|
+
logger=logger,
|
|
30
|
+
timeout=timeout,
|
|
31
|
+
api_key=api_key,
|
|
32
|
+
base_url=base_url,
|
|
33
|
+
bearer_token=bearer_token,
|
|
34
|
+
log_requests=log_requests,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _cleanup_session(session: aiohttp.ClientSession):
|
|
39
|
+
"""Static method to clean up session - called by finalizer"""
|
|
40
|
+
if session and not session.closed:
|
|
41
|
+
# Create a new event loop if none exists
|
|
42
|
+
try:
|
|
43
|
+
loop = asyncio.get_event_loop()
|
|
44
|
+
if loop.is_closed():
|
|
45
|
+
raise RuntimeError("Event loop is closed")
|
|
46
|
+
except RuntimeError:
|
|
47
|
+
loop = asyncio.new_event_loop()
|
|
48
|
+
asyncio.set_event_loop(loop)
|
|
49
|
+
|
|
50
|
+
# Close the session
|
|
51
|
+
if not loop.is_running():
|
|
52
|
+
loop.run_until_complete(session.close())
|
|
53
|
+
else:
|
|
54
|
+
# If loop is running, schedule the close operation
|
|
55
|
+
asyncio.create_task(session.close())
|
|
56
|
+
|
|
57
|
+
async def close(self):
|
|
58
|
+
"""
|
|
59
|
+
Closes the aiohttp session to free up system resources.
|
|
60
|
+
|
|
61
|
+
This method should be called when the RequestHandler is no longer needed to ensure
|
|
62
|
+
proper cleanup of the underlying session and its resources.
|
|
63
|
+
"""
|
|
64
|
+
if self.session and not self.session.closed:
|
|
65
|
+
await self.session.close()
|
|
66
|
+
|
|
67
|
+
def _handle_exception(
|
|
68
|
+
self, e: aiohttp.ClientResponseError, url: str, correlation_id: str
|
|
69
|
+
):
|
|
70
|
+
# Log the error response if enabled
|
|
71
|
+
if self.log_requests:
|
|
72
|
+
self.logger.error(
|
|
73
|
+
f"API Error: {e.status} {e.message}\n"
|
|
74
|
+
f"URL: {url}\n"
|
|
75
|
+
f"Correlation ID: {correlation_id}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Extract error details from response
|
|
79
|
+
error_message = e.message
|
|
80
|
+
|
|
81
|
+
# Make sure sensitive auth information is not included in error messages
|
|
82
|
+
sanitized_message = error_message
|
|
83
|
+
if self.api_key and self.api_key in sanitized_message:
|
|
84
|
+
sanitized_message = sanitized_message.replace(self.api_key, "[REDACTED]")
|
|
85
|
+
if self.bearer_token and self.bearer_token in sanitized_message:
|
|
86
|
+
sanitized_message = sanitized_message.replace(
|
|
87
|
+
self.bearer_token, "[REDACTED]"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Raise custom exception with status code and sanitized message
|
|
91
|
+
raise AiriaAPIError(status_code=e.status, message=sanitized_message) from e
|
|
92
|
+
|
|
93
|
+
async def make_request(
|
|
94
|
+
self, method: str, request_data: RequestData, return_json: bool = True
|
|
95
|
+
) -> Optional[Dict[str, Any]]:
|
|
96
|
+
"""
|
|
97
|
+
Makes an asynchronous HTTP request to the Airia API.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
method (str): The HTTP method (e.g., 'GET', 'POST')
|
|
101
|
+
request_data: A dictionary containing the following request information:
|
|
102
|
+
- url: The endpoint URL for the request
|
|
103
|
+
- headers: HTTP headers to include in the request
|
|
104
|
+
- payload: The JSON payload/body for the request
|
|
105
|
+
- params: Optional query parameters to append to the URL
|
|
106
|
+
- files: Optional file data to be uploaded in the request body
|
|
107
|
+
- correlation_id: Unique identifier for request tracing
|
|
108
|
+
return_json (bool): Whether to return the response as JSON. Default is True.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
resp ([Dict[str, Any]): The JSON response from the API as a dictionary.
|
|
112
|
+
|
|
113
|
+
Raises:
|
|
114
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
115
|
+
aiohttp.ClientResponseError: For HTTP-related errors
|
|
116
|
+
|
|
117
|
+
Note:
|
|
118
|
+
This is an internal method used by other client methods to make API requests.
|
|
119
|
+
It handles logging, error handling, and API key redaction in error messages.
|
|
120
|
+
"""
|
|
121
|
+
try:
|
|
122
|
+
# Make the request
|
|
123
|
+
async with self.session.request(
|
|
124
|
+
method=method,
|
|
125
|
+
url=request_data.url,
|
|
126
|
+
json=request_data.payload,
|
|
127
|
+
params=request_data.params,
|
|
128
|
+
headers=request_data.headers,
|
|
129
|
+
timeout=self.timeout,
|
|
130
|
+
) as response:
|
|
131
|
+
# Log the response if enabled
|
|
132
|
+
if self.log_requests:
|
|
133
|
+
self.logger.info(
|
|
134
|
+
f"API Response: {response.status} {response.reason}\n"
|
|
135
|
+
f"URL: {request_data.url}\n"
|
|
136
|
+
f"Correlation ID: {request_data.correlation_id}"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check for HTTP errors
|
|
140
|
+
response.raise_for_status()
|
|
141
|
+
|
|
142
|
+
# Return the response as a dictionary
|
|
143
|
+
if return_json:
|
|
144
|
+
return await response.json()
|
|
145
|
+
|
|
146
|
+
except aiohttp.ClientResponseError as e:
|
|
147
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|
|
148
|
+
|
|
149
|
+
async def make_request_stream(
|
|
150
|
+
self, method: str, request_data: RequestData
|
|
151
|
+
) -> AsyncIterator[str]:
|
|
152
|
+
"""
|
|
153
|
+
Makes an asynchronous HTTP request to the Airia API.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
method (str): The HTTP method (e.g., 'GET', 'POST')
|
|
157
|
+
request_data: A dictionary containing the following request information:
|
|
158
|
+
- url: The endpoint URL for the request
|
|
159
|
+
- headers: HTTP headers to include in the request
|
|
160
|
+
- payload: The JSON payload/body for the request
|
|
161
|
+
- params: Optional query parameters to append to the URL
|
|
162
|
+
- files: Optional file data to be uploaded in the request body
|
|
163
|
+
- correlation_id: Unique identifier for request tracing
|
|
164
|
+
|
|
165
|
+
Yields:
|
|
166
|
+
resp AsyncIterator[str]]: yields chunks of the response as they are received.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
170
|
+
aiohttp.ClientResponseError: For HTTP-related errors
|
|
171
|
+
|
|
172
|
+
Note:
|
|
173
|
+
This is an internal method used by other client methods to make API requests.
|
|
174
|
+
It handles logging, error handling, and API key redaction in error messages.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
# Make the request
|
|
178
|
+
async with self.session.request(
|
|
179
|
+
method=method,
|
|
180
|
+
url=request_data.url,
|
|
181
|
+
json=request_data.payload,
|
|
182
|
+
params=request_data.params,
|
|
183
|
+
headers=request_data.headers,
|
|
184
|
+
timeout=self.timeout,
|
|
185
|
+
chunked=True,
|
|
186
|
+
) as response:
|
|
187
|
+
# Log the response if enabled
|
|
188
|
+
if self.log_requests:
|
|
189
|
+
self.logger.info(
|
|
190
|
+
f"API Response: {response.status} {response.reason}\n"
|
|
191
|
+
f"URL: {request_data.url}\n"
|
|
192
|
+
f"Correlation ID: {request_data.correlation_id}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Check for HTTP errors
|
|
196
|
+
response.raise_for_status()
|
|
197
|
+
|
|
198
|
+
# Yields the response content as a stream if streaming
|
|
199
|
+
async for message in async_parse_sse_stream_chunked(
|
|
200
|
+
response.content.iter_any()
|
|
201
|
+
):
|
|
202
|
+
yield message
|
|
203
|
+
|
|
204
|
+
except aiohttp.ClientResponseError as e:
|
|
205
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|
|
206
|
+
|
|
207
|
+
async def make_request_multipart(
|
|
208
|
+
self, method: str, request_data: RequestData, return_json: bool = True
|
|
209
|
+
) -> Optional[Dict[str, Any]]:
|
|
210
|
+
"""
|
|
211
|
+
Makes an asynchronous HTTP request with multipart form data to the Airia API.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
method (str): The HTTP method (e.g., 'POST')
|
|
215
|
+
request_data: A dictionary containing the following request information:
|
|
216
|
+
- url: The endpoint URL for the request
|
|
217
|
+
- headers: HTTP headers to include in the request
|
|
218
|
+
- payload: The form data payload including file content
|
|
219
|
+
- params: Optional query parameters to append to the URL
|
|
220
|
+
- files: Optional file data to be uploaded in the request body
|
|
221
|
+
- correlation_id: Unique identifier for request tracing
|
|
222
|
+
return_json (bool): Whether to return the response as JSON. Default is True.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
resp (Optional[Dict[str, Any]]): The JSON response from the API as a dictionary.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
229
|
+
aiohttp.ClientResponseError: For HTTP-related errors
|
|
230
|
+
|
|
231
|
+
Note:
|
|
232
|
+
This is an internal method used by file upload methods to make multipart requests.
|
|
233
|
+
It handles multipart form data encoding, logging, and error handling.
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
# Prepare multipart form data
|
|
237
|
+
data = aiohttp.FormData()
|
|
238
|
+
|
|
239
|
+
# Add form fields
|
|
240
|
+
for key, value in request_data.payload.items():
|
|
241
|
+
data.add_field(key, str(value))
|
|
242
|
+
|
|
243
|
+
# Add files
|
|
244
|
+
for key, value in request_data.files.items():
|
|
245
|
+
data.add_field(key, value[1], filename=value[0], content_type=value[2])
|
|
246
|
+
|
|
247
|
+
# Make the request
|
|
248
|
+
async with self.session.request(
|
|
249
|
+
method=method,
|
|
250
|
+
url=request_data.url,
|
|
251
|
+
data=data,
|
|
252
|
+
params=request_data.params,
|
|
253
|
+
headers=request_data.headers,
|
|
254
|
+
timeout=self.timeout,
|
|
255
|
+
) as response:
|
|
256
|
+
# Log the response if enabled
|
|
257
|
+
if self.log_requests:
|
|
258
|
+
self.logger.info(
|
|
259
|
+
f"API Response: {response.status} {response.reason}\n"
|
|
260
|
+
f"URL: {request_data.url}\n"
|
|
261
|
+
f"Correlation ID: {request_data.correlation_id}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# Check for HTTP errors
|
|
265
|
+
response.raise_for_status()
|
|
266
|
+
|
|
267
|
+
# Return the response as a dictionary
|
|
268
|
+
if return_json:
|
|
269
|
+
return await response.json()
|
|
270
|
+
|
|
271
|
+
except aiohttp.ClientResponseError as e:
|
|
272
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import loguru
|
|
5
|
+
|
|
6
|
+
from ...logs import set_correlation_id
|
|
7
|
+
from ...types._request_data import RequestData
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseRequestHandler:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
logger: "loguru.Logger",
|
|
14
|
+
timeout: float,
|
|
15
|
+
base_url: str,
|
|
16
|
+
api_key: Optional[str] = None,
|
|
17
|
+
bearer_token: Optional[str] = None,
|
|
18
|
+
log_requests: bool = False,
|
|
19
|
+
):
|
|
20
|
+
self.logger = logger
|
|
21
|
+
self.timeout = timeout
|
|
22
|
+
self.base_url = base_url
|
|
23
|
+
self.api_key = api_key
|
|
24
|
+
self.bearer_token = bearer_token
|
|
25
|
+
self.log_requests = log_requests
|
|
26
|
+
|
|
27
|
+
def close(self):
|
|
28
|
+
raise NotImplementedError("Subclasses must implement this method")
|
|
29
|
+
|
|
30
|
+
def prepare_request(
|
|
31
|
+
self,
|
|
32
|
+
url: str,
|
|
33
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
34
|
+
params: Optional[Dict[str, Any]] = None,
|
|
35
|
+
files: Optional[Dict[str, Any]] = None,
|
|
36
|
+
correlation_id: Optional[str] = None,
|
|
37
|
+
):
|
|
38
|
+
"""
|
|
39
|
+
Prepare request data including headers, authentication, and logging.
|
|
40
|
+
|
|
41
|
+
This method sets up all the necessary components for an API request including
|
|
42
|
+
correlation ID, authentication headers, and sanitized logging.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
url: The target URL for the request
|
|
46
|
+
payload: Optional JSON payload for the request body
|
|
47
|
+
params: Optional query parameters for the request
|
|
48
|
+
files: Optional files to be uploaded in the request body
|
|
49
|
+
correlation_id: Optional correlation ID for request tracing
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
RequestData: A data structure containing all prepared request components
|
|
53
|
+
"""
|
|
54
|
+
# Set correlation ID if provided or generate a new one
|
|
55
|
+
correlation_id = set_correlation_id(correlation_id)
|
|
56
|
+
|
|
57
|
+
# Set up base headers
|
|
58
|
+
headers = {"X-Correlation-ID": correlation_id}
|
|
59
|
+
|
|
60
|
+
# Add authentication header based on the method used
|
|
61
|
+
if self.api_key:
|
|
62
|
+
headers["X-API-KEY"] = self.api_key
|
|
63
|
+
elif self.bearer_token:
|
|
64
|
+
headers["Authorization"] = f"Bearer {self.bearer_token}"
|
|
65
|
+
|
|
66
|
+
# Log the request if enabled
|
|
67
|
+
if self.log_requests:
|
|
68
|
+
# Create a sanitized copy of headers and params for logging
|
|
69
|
+
log_headers = headers.copy()
|
|
70
|
+
log_params = params.copy() if params is not None else {}
|
|
71
|
+
log_files = (
|
|
72
|
+
{k: (v[0], "[BINARY DATA]", v[2]) for k, v in files.items()}
|
|
73
|
+
if files is not None
|
|
74
|
+
else {}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Filter out sensitive headers
|
|
78
|
+
if "X-API-KEY" in log_headers:
|
|
79
|
+
log_headers["X-API-KEY"] = "[REDACTED]"
|
|
80
|
+
if "Authorization" in log_headers:
|
|
81
|
+
log_headers["Authorization"] = "[REDACTED]"
|
|
82
|
+
|
|
83
|
+
# Process payload for logging
|
|
84
|
+
log_payload = payload.copy() if payload is not None else {}
|
|
85
|
+
if "images" in log_payload and log_payload["images"] is not None:
|
|
86
|
+
log_payload["images"] = f"{len(log_payload['images'])} images"
|
|
87
|
+
if "files" in log_payload and log_payload["files"] is not None:
|
|
88
|
+
log_payload["files"] = f"{len(log_payload['files'])} files"
|
|
89
|
+
log_payload = json.dumps(log_payload)
|
|
90
|
+
|
|
91
|
+
self.logger.info(
|
|
92
|
+
f"API Request: POST {url}\n"
|
|
93
|
+
f"Headers: {json.dumps(log_headers)}\n"
|
|
94
|
+
f"Payload: {log_payload}\n"
|
|
95
|
+
f"Files: {log_files}\n"
|
|
96
|
+
f"Params: {json.dumps(log_params)}\n"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
return RequestData(
|
|
100
|
+
**{
|
|
101
|
+
"url": url,
|
|
102
|
+
"payload": payload,
|
|
103
|
+
"headers": headers,
|
|
104
|
+
"params": params,
|
|
105
|
+
"files": files,
|
|
106
|
+
"correlation_id": correlation_id,
|
|
107
|
+
}
|
|
108
|
+
)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import weakref
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import loguru
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from ...exceptions import AiriaAPIError
|
|
8
|
+
from ...types._request_data import RequestData
|
|
9
|
+
from ...utils.sse_parser import parse_sse_stream_chunked
|
|
10
|
+
from .base_request_handler import BaseRequestHandler
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RequestHandler(BaseRequestHandler):
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
logger: "loguru.Logger",
|
|
17
|
+
timeout: float,
|
|
18
|
+
base_url: str,
|
|
19
|
+
api_key: Optional[str] = None,
|
|
20
|
+
bearer_token: Optional[str] = None,
|
|
21
|
+
log_requests: bool = False,
|
|
22
|
+
):
|
|
23
|
+
# Initialize session for synchronous requests
|
|
24
|
+
self.session = requests.Session()
|
|
25
|
+
self._finalizer = weakref.finalize(self, self._cleanup_session, self.session)
|
|
26
|
+
|
|
27
|
+
super().__init__(
|
|
28
|
+
logger=logger,
|
|
29
|
+
timeout=timeout,
|
|
30
|
+
api_key=api_key,
|
|
31
|
+
base_url=base_url,
|
|
32
|
+
bearer_token=bearer_token,
|
|
33
|
+
log_requests=log_requests,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _cleanup_session(session: requests.Session):
|
|
38
|
+
"""Static method to clean up session - called by finalizer"""
|
|
39
|
+
if session:
|
|
40
|
+
session.close()
|
|
41
|
+
|
|
42
|
+
def close(self):
|
|
43
|
+
"""
|
|
44
|
+
Closes the requests session to free up system resources.
|
|
45
|
+
|
|
46
|
+
This method should be called when the RequestHandler is no longer needed to ensure
|
|
47
|
+
proper cleanup of the underlying session and its resources.
|
|
48
|
+
"""
|
|
49
|
+
self.session.close()
|
|
50
|
+
|
|
51
|
+
def _handle_exception(self, e: requests.HTTPError, url: str, correlation_id: str):
|
|
52
|
+
# Log the error response if enabled
|
|
53
|
+
if self.log_requests:
|
|
54
|
+
self.logger.error(
|
|
55
|
+
f"API Error: {e.response.status_code} {e.response.reason}\n"
|
|
56
|
+
f"URL: {url}\n"
|
|
57
|
+
f"Correlation ID: {correlation_id}"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Extract error details from response if possible
|
|
61
|
+
error_message = "API request failed"
|
|
62
|
+
try:
|
|
63
|
+
error_data = e.response.json()
|
|
64
|
+
if isinstance(error_data, dict) and "message" in error_data:
|
|
65
|
+
error_message = error_data["message"]
|
|
66
|
+
elif isinstance(error_data, dict) and "error" in error_data:
|
|
67
|
+
error_message = error_data["error"]
|
|
68
|
+
except (ValueError, KeyError):
|
|
69
|
+
# If JSON parsing fails or expected keys are missing
|
|
70
|
+
error_message = f"API request failed: {str(e)}"
|
|
71
|
+
|
|
72
|
+
# Make sure sensitive auth information is not included in error messages
|
|
73
|
+
sanitized_message = error_message
|
|
74
|
+
if self.api_key and self.api_key in sanitized_message:
|
|
75
|
+
sanitized_message = sanitized_message.replace(self.api_key, "[REDACTED]")
|
|
76
|
+
if self.bearer_token and self.bearer_token in sanitized_message:
|
|
77
|
+
sanitized_message = sanitized_message.replace(
|
|
78
|
+
self.bearer_token, "[REDACTED]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Raise custom exception with status code and sanitized message
|
|
82
|
+
raise AiriaAPIError(
|
|
83
|
+
status_code=e.response.status_code, message=sanitized_message
|
|
84
|
+
) from e
|
|
85
|
+
|
|
86
|
+
def make_request(
|
|
87
|
+
self, method: str, request_data: RequestData, return_json: bool = True
|
|
88
|
+
) -> Dict[str, Any]:
|
|
89
|
+
"""
|
|
90
|
+
Makes a synchronous HTTP request to the Airia API.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
method (str): The HTTP method (e.g., 'GET', 'POST')
|
|
94
|
+
request_data: A dictionary containing the following request information:
|
|
95
|
+
- url: The endpoint URL for the request
|
|
96
|
+
- headers: HTTP headers to include in the request
|
|
97
|
+
- payload: The JSON payload/body for the request
|
|
98
|
+
- params: Optional query parameters to append to the URL
|
|
99
|
+
- files: Optional file data to be uploaded in the request body
|
|
100
|
+
- correlation_id: Unique identifier for request tracing
|
|
101
|
+
return_json (bool): Whether to return the response as JSON. Default is True.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
resp (Dict[str, Any]): The JSON response from the API as a dictionary.
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
108
|
+
requests.HTTPError: For HTTP-related errors
|
|
109
|
+
|
|
110
|
+
Note:
|
|
111
|
+
This is an internal method used by other client methods to make API requests.
|
|
112
|
+
It handles logging, error handling, and API key redaction in error messages.
|
|
113
|
+
"""
|
|
114
|
+
try:
|
|
115
|
+
# Make the request
|
|
116
|
+
response = self.session.request(
|
|
117
|
+
method=method,
|
|
118
|
+
url=request_data.url,
|
|
119
|
+
json=request_data.payload,
|
|
120
|
+
params=request_data.params,
|
|
121
|
+
headers=request_data.headers,
|
|
122
|
+
timeout=self.timeout,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Log the response if enabled
|
|
126
|
+
if self.log_requests:
|
|
127
|
+
self.logger.info(
|
|
128
|
+
f"API Response: {response.status_code} {response.reason}\n"
|
|
129
|
+
f"URL: {request_data.url}\n"
|
|
130
|
+
f"Correlation ID: {request_data.correlation_id}\n"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Check for HTTP errors
|
|
134
|
+
response.raise_for_status()
|
|
135
|
+
|
|
136
|
+
# Returns the JSON response
|
|
137
|
+
if return_json:
|
|
138
|
+
return response.json()
|
|
139
|
+
|
|
140
|
+
except requests.HTTPError as e:
|
|
141
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|
|
142
|
+
|
|
143
|
+
def make_request_stream(self, method: str, request_data: RequestData):
|
|
144
|
+
"""
|
|
145
|
+
Makes a synchronous HTTP request to the Airia API.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
method (str): The HTTP method (e.g., 'GET', 'POST')
|
|
149
|
+
request_data: A dictionary containing the following request information:
|
|
150
|
+
- url: The endpoint URL for the request
|
|
151
|
+
- headers: HTTP headers to include in the request
|
|
152
|
+
- payload: The JSON payload/body for the request
|
|
153
|
+
- params: Optional query parameters to append to the URL
|
|
154
|
+
- files: Optional file data to be uploaded in the request body
|
|
155
|
+
- correlation_id: Unique identifier for request tracing
|
|
156
|
+
stream (bool): If True, the response will be streamed instead of downloaded all at once
|
|
157
|
+
|
|
158
|
+
Yields:
|
|
159
|
+
resp (Iterator[str]): Yields chunks of the response as they are received.
|
|
160
|
+
|
|
161
|
+
Raises:
|
|
162
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
163
|
+
requests.HTTPError: For HTTP-related errors
|
|
164
|
+
|
|
165
|
+
Note:
|
|
166
|
+
This is an internal method used by other client methods to make API requests.
|
|
167
|
+
It handles logging, error handling, and API key redaction in error messages.
|
|
168
|
+
"""
|
|
169
|
+
try:
|
|
170
|
+
# Make the request
|
|
171
|
+
response = self.session.request(
|
|
172
|
+
method=method,
|
|
173
|
+
url=request_data.url,
|
|
174
|
+
params=request_data.params,
|
|
175
|
+
json=request_data.payload,
|
|
176
|
+
headers=request_data.headers,
|
|
177
|
+
timeout=self.timeout,
|
|
178
|
+
stream=True,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Log the response if enabled
|
|
182
|
+
if self.log_requests:
|
|
183
|
+
self.logger.info(
|
|
184
|
+
f"API Response: {response.status_code} {response.reason}\n"
|
|
185
|
+
f"URL: {request_data.url}\n"
|
|
186
|
+
f"Correlation ID: {request_data.correlation_id}\n"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Check for HTTP errors
|
|
190
|
+
response.raise_for_status()
|
|
191
|
+
|
|
192
|
+
# Yields the response content as a stream
|
|
193
|
+
for message in parse_sse_stream_chunked(response.iter_content()):
|
|
194
|
+
yield message
|
|
195
|
+
|
|
196
|
+
except requests.HTTPError as e:
|
|
197
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|
|
198
|
+
|
|
199
|
+
def make_request_multipart(
|
|
200
|
+
self, method: str, request_data: RequestData, return_json: bool = True
|
|
201
|
+
):
|
|
202
|
+
"""
|
|
203
|
+
Makes a synchronous HTTP request with multipart form data to the Airia API.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
method (str): The HTTP method (e.g., 'POST')
|
|
207
|
+
request_data: A dictionary containing the following request information:
|
|
208
|
+
- url: The endpoint URL for the request
|
|
209
|
+
- headers: HTTP headers to include in the request
|
|
210
|
+
- payload: The form data payload including file content
|
|
211
|
+
- params: Optional query parameters to append to the URL
|
|
212
|
+
- files: Optional file data to be uploaded in the request body
|
|
213
|
+
- correlation_id: Unique identifier for request tracing
|
|
214
|
+
return_json (bool): Whether to return the response as JSON. Default is True.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
resp (Dict[str, Any]): The JSON response from the API as a dictionary.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
AiriaAPIError: If the API returns an error response, with details about the error
|
|
221
|
+
requests.HTTPError: For HTTP-related errors
|
|
222
|
+
|
|
223
|
+
Note:
|
|
224
|
+
This is an internal method used by file upload methods to make multipart requests.
|
|
225
|
+
It handles multipart form data encoding, logging, and error handling.
|
|
226
|
+
"""
|
|
227
|
+
try:
|
|
228
|
+
# Make the request
|
|
229
|
+
response = self.session.request(
|
|
230
|
+
method=method,
|
|
231
|
+
url=request_data.url,
|
|
232
|
+
files=request_data.files,
|
|
233
|
+
data=request_data.payload,
|
|
234
|
+
params=request_data.params,
|
|
235
|
+
headers=request_data.headers,
|
|
236
|
+
timeout=self.timeout,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Log the response if enabled
|
|
240
|
+
if self.log_requests:
|
|
241
|
+
self.logger.info(
|
|
242
|
+
f"API Response: {response.status_code} {response.reason}\n"
|
|
243
|
+
f"URL: {request_data.url}\n"
|
|
244
|
+
f"Correlation ID: {request_data.correlation_id}\n"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
# Check for HTTP errors
|
|
248
|
+
response.raise_for_status()
|
|
249
|
+
|
|
250
|
+
# Returns the JSON response
|
|
251
|
+
if return_json:
|
|
252
|
+
return response.json()
|
|
253
|
+
|
|
254
|
+
except requests.HTTPError as e:
|
|
255
|
+
self._handle_exception(e, request_data.url, request_data.correlation_id)
|