binalyze-air-sdk 1.0.2__py3-none-any.whl → 1.0.3__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.
- binalyze_air/__init__.py +77 -77
- binalyze_air/apis/__init__.py +67 -27
- binalyze_air/apis/acquisitions.py +107 -0
- binalyze_air/apis/api_tokens.py +49 -0
- binalyze_air/apis/assets.py +161 -0
- binalyze_air/apis/audit_logs.py +26 -0
- binalyze_air/apis/{authentication.py → auth.py} +29 -27
- binalyze_air/apis/auto_asset_tags.py +79 -75
- binalyze_air/apis/backup.py +177 -0
- binalyze_air/apis/baseline.py +46 -0
- binalyze_air/apis/cases.py +225 -0
- binalyze_air/apis/cloud_forensics.py +116 -0
- binalyze_air/apis/event_subscription.py +96 -96
- binalyze_air/apis/evidence.py +249 -53
- binalyze_air/apis/interact.py +153 -36
- binalyze_air/apis/investigation_hub.py +234 -0
- binalyze_air/apis/license.py +104 -0
- binalyze_air/apis/logger.py +83 -0
- binalyze_air/apis/multipart_upload.py +201 -0
- binalyze_air/apis/notifications.py +115 -0
- binalyze_air/apis/organizations.py +267 -0
- binalyze_air/apis/params.py +44 -39
- binalyze_air/apis/policies.py +186 -0
- binalyze_air/apis/preset_filters.py +79 -0
- binalyze_air/apis/recent_activities.py +71 -0
- binalyze_air/apis/relay_server.py +104 -0
- binalyze_air/apis/settings.py +395 -27
- binalyze_air/apis/tasks.py +80 -0
- binalyze_air/apis/triage.py +197 -0
- binalyze_air/apis/user_management.py +183 -74
- binalyze_air/apis/webhook_executions.py +50 -0
- binalyze_air/apis/webhooks.py +322 -230
- binalyze_air/base.py +207 -133
- binalyze_air/client.py +217 -1337
- binalyze_air/commands/__init__.py +175 -145
- binalyze_air/commands/acquisitions.py +661 -387
- binalyze_air/commands/api_tokens.py +55 -0
- binalyze_air/commands/assets.py +324 -362
- binalyze_air/commands/{authentication.py → auth.py} +36 -36
- binalyze_air/commands/auto_asset_tags.py +230 -230
- binalyze_air/commands/backup.py +47 -0
- binalyze_air/commands/baseline.py +32 -396
- binalyze_air/commands/cases.py +609 -602
- binalyze_air/commands/cloud_forensics.py +88 -0
- binalyze_air/commands/event_subscription.py +101 -101
- binalyze_air/commands/evidences.py +918 -988
- binalyze_air/commands/interact.py +172 -58
- binalyze_air/commands/investigation_hub.py +315 -0
- binalyze_air/commands/license.py +183 -0
- binalyze_air/commands/logger.py +126 -0
- binalyze_air/commands/multipart_upload.py +363 -0
- binalyze_air/commands/notifications.py +45 -0
- binalyze_air/commands/organizations.py +200 -221
- binalyze_air/commands/policies.py +175 -203
- binalyze_air/commands/preset_filters.py +55 -0
- binalyze_air/commands/recent_activities.py +32 -0
- binalyze_air/commands/relay_server.py +144 -0
- binalyze_air/commands/settings.py +431 -29
- binalyze_air/commands/tasks.py +95 -56
- binalyze_air/commands/triage.py +224 -360
- binalyze_air/commands/user_management.py +351 -126
- binalyze_air/commands/webhook_executions.py +77 -0
- binalyze_air/config.py +244 -244
- binalyze_air/exceptions.py +49 -49
- binalyze_air/http_client.py +426 -305
- binalyze_air/models/__init__.py +287 -285
- binalyze_air/models/acquisitions.py +365 -250
- binalyze_air/models/api_tokens.py +73 -0
- binalyze_air/models/assets.py +438 -438
- binalyze_air/models/audit.py +247 -272
- binalyze_air/models/audit_logs.py +14 -0
- binalyze_air/models/{authentication.py → auth.py} +69 -69
- binalyze_air/models/auto_asset_tags.py +227 -116
- binalyze_air/models/backup.py +138 -0
- binalyze_air/models/baseline.py +231 -231
- binalyze_air/models/cases.py +275 -275
- binalyze_air/models/cloud_forensics.py +145 -0
- binalyze_air/models/event_subscription.py +170 -171
- binalyze_air/models/evidence.py +65 -65
- binalyze_air/models/evidences.py +367 -348
- binalyze_air/models/interact.py +266 -135
- binalyze_air/models/investigation_hub.py +265 -0
- binalyze_air/models/license.py +150 -0
- binalyze_air/models/logger.py +83 -0
- binalyze_air/models/multipart_upload.py +352 -0
- binalyze_air/models/notifications.py +138 -0
- binalyze_air/models/organizations.py +293 -293
- binalyze_air/models/params.py +153 -127
- binalyze_air/models/policies.py +260 -249
- binalyze_air/models/preset_filters.py +79 -0
- binalyze_air/models/recent_activities.py +70 -0
- binalyze_air/models/relay_server.py +121 -0
- binalyze_air/models/settings.py +538 -84
- binalyze_air/models/tasks.py +215 -149
- binalyze_air/models/triage.py +141 -142
- binalyze_air/models/user_management.py +200 -97
- binalyze_air/models/webhook_executions.py +33 -0
- binalyze_air/queries/__init__.py +121 -133
- binalyze_air/queries/acquisitions.py +155 -155
- binalyze_air/queries/api_tokens.py +46 -0
- binalyze_air/queries/assets.py +186 -105
- binalyze_air/queries/audit.py +400 -416
- binalyze_air/queries/{authentication.py → auth.py} +55 -55
- binalyze_air/queries/auto_asset_tags.py +59 -59
- binalyze_air/queries/backup.py +66 -0
- binalyze_air/queries/baseline.py +21 -185
- binalyze_air/queries/cases.py +292 -292
- binalyze_air/queries/cloud_forensics.py +137 -0
- binalyze_air/queries/event_subscription.py +54 -54
- binalyze_air/queries/evidence.py +139 -139
- binalyze_air/queries/evidences.py +279 -279
- binalyze_air/queries/interact.py +140 -28
- binalyze_air/queries/investigation_hub.py +329 -0
- binalyze_air/queries/license.py +85 -0
- binalyze_air/queries/logger.py +58 -0
- binalyze_air/queries/multipart_upload.py +180 -0
- binalyze_air/queries/notifications.py +71 -0
- binalyze_air/queries/organizations.py +222 -222
- binalyze_air/queries/params.py +154 -115
- binalyze_air/queries/policies.py +149 -149
- binalyze_air/queries/preset_filters.py +60 -0
- binalyze_air/queries/recent_activities.py +44 -0
- binalyze_air/queries/relay_server.py +42 -0
- binalyze_air/queries/settings.py +533 -20
- binalyze_air/queries/tasks.py +125 -81
- binalyze_air/queries/triage.py +230 -230
- binalyze_air/queries/user_management.py +193 -83
- binalyze_air/queries/webhook_executions.py +39 -0
- binalyze_air_sdk-1.0.3.dist-info/METADATA +752 -0
- binalyze_air_sdk-1.0.3.dist-info/RECORD +132 -0
- {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/WHEEL +1 -1
- binalyze_air/apis/endpoints.py +0 -22
- binalyze_air/apis/evidences.py +0 -216
- binalyze_air/apis/users.py +0 -68
- binalyze_air/commands/users.py +0 -101
- binalyze_air/models/endpoints.py +0 -76
- binalyze_air/models/users.py +0 -82
- binalyze_air/queries/endpoints.py +0 -25
- binalyze_air/queries/users.py +0 -69
- binalyze_air_sdk-1.0.2.dist-info/METADATA +0 -706
- binalyze_air_sdk-1.0.2.dist-info/RECORD +0 -82
- {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
binalyze_air/http_client.py
CHANGED
@@ -1,306 +1,427 @@
|
|
1
|
-
"""
|
2
|
-
HTTP client for Binalyze AIR API communications.
|
3
|
-
"""
|
4
|
-
|
5
|
-
import time
|
6
|
-
import requests
|
7
|
-
import urllib3
|
8
|
-
from typing import Any, Dict, Optional, Union
|
9
|
-
from urllib.parse import urljoin
|
10
|
-
|
11
|
-
from .config import AIRConfig
|
12
|
-
from .exceptions import (
|
13
|
-
AIRAPIError,
|
14
|
-
AuthenticationError,
|
15
|
-
AuthorizationError,
|
16
|
-
NotFoundError,
|
17
|
-
ValidationError,
|
18
|
-
RateLimitError,
|
19
|
-
ServerError,
|
20
|
-
NetworkError,
|
21
|
-
)
|
22
|
-
|
23
|
-
|
24
|
-
class HTTPClient:
|
25
|
-
"""HTTP client for AIR API communications."""
|
26
|
-
|
27
|
-
def __init__(self, config: AIRConfig):
|
28
|
-
"""Initialize the HTTP client with configuration."""
|
29
|
-
self.config = config
|
30
|
-
self.session = requests.Session()
|
31
|
-
self.session.headers.update({
|
32
|
-
"Content-Type": "application/json",
|
33
|
-
"Authorization": f"Bearer {config.api_token}",
|
34
|
-
"User-Agent": "binalyze-air-sdk/1.0.0",
|
35
|
-
})
|
36
|
-
self.session.verify = config.verify_ssl
|
37
|
-
|
38
|
-
# Disable SSL warnings when SSL verification is disabled
|
39
|
-
if not config.verify_ssl:
|
40
|
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
41
|
-
|
42
|
-
def _build_url(self, endpoint: str) -> str:
|
43
|
-
"""Build full URL from endpoint."""
|
44
|
-
# Remove leading slash if present
|
45
|
-
endpoint = endpoint.lstrip("/")
|
46
|
-
# Build full URL with API prefix
|
47
|
-
return f"{self.config.host}/{self.config.api_prefix}/{endpoint}"
|
48
|
-
|
49
|
-
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
50
|
-
"""Handle HTTP response and raise appropriate exceptions."""
|
51
|
-
try:
|
52
|
-
data = response.json()
|
53
|
-
except ValueError:
|
54
|
-
# If response is not JSON, create a basic structure
|
55
|
-
data = {
|
56
|
-
"success": False,
|
57
|
-
"result": None,
|
58
|
-
"statusCode": response.status_code,
|
59
|
-
"errors": [response.text or "Unknown error"]
|
60
|
-
}
|
61
|
-
|
62
|
-
#
|
63
|
-
if response.status_code ==
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
"
|
80
|
-
"
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
)
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
for
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
raise
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
endpoint: str,
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
) -> Dict[str, Any]:
|
237
|
-
"""Make
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
self
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
last_exception
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
1
|
+
"""
|
2
|
+
HTTP client for Binalyze AIR API communications.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
import requests
|
7
|
+
import urllib3
|
8
|
+
from typing import Any, Dict, Optional, Union
|
9
|
+
from urllib.parse import urljoin
|
10
|
+
|
11
|
+
from .config import AIRConfig
|
12
|
+
from .exceptions import (
|
13
|
+
AIRAPIError,
|
14
|
+
AuthenticationError,
|
15
|
+
AuthorizationError,
|
16
|
+
NotFoundError,
|
17
|
+
ValidationError,
|
18
|
+
RateLimitError,
|
19
|
+
ServerError,
|
20
|
+
NetworkError,
|
21
|
+
)
|
22
|
+
|
23
|
+
|
24
|
+
class HTTPClient:
|
25
|
+
"""HTTP client for AIR API communications."""
|
26
|
+
|
27
|
+
def __init__(self, config: AIRConfig):
|
28
|
+
"""Initialize the HTTP client with configuration."""
|
29
|
+
self.config = config
|
30
|
+
self.session = requests.Session()
|
31
|
+
self.session.headers.update({
|
32
|
+
"Content-Type": "application/json",
|
33
|
+
"Authorization": f"Bearer {config.api_token}",
|
34
|
+
"User-Agent": "binalyze-air-sdk/1.0.0",
|
35
|
+
})
|
36
|
+
self.session.verify = config.verify_ssl
|
37
|
+
|
38
|
+
# Disable SSL warnings when SSL verification is disabled
|
39
|
+
if not config.verify_ssl:
|
40
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
41
|
+
|
42
|
+
def _build_url(self, endpoint: str) -> str:
|
43
|
+
"""Build full URL from endpoint."""
|
44
|
+
# Remove leading slash if present
|
45
|
+
endpoint = endpoint.lstrip("/")
|
46
|
+
# Build full URL with API prefix
|
47
|
+
return f"{self.config.host}/{self.config.api_prefix}/{endpoint}"
|
48
|
+
|
49
|
+
def _handle_response(self, response: requests.Response) -> Dict[str, Any]:
|
50
|
+
"""Handle HTTP response and raise appropriate exceptions."""
|
51
|
+
try:
|
52
|
+
data = response.json()
|
53
|
+
except ValueError:
|
54
|
+
# If response is not JSON, create a basic structure
|
55
|
+
data = {
|
56
|
+
"success": False,
|
57
|
+
"result": None,
|
58
|
+
"statusCode": response.status_code,
|
59
|
+
"errors": [response.text or "Unknown error"]
|
60
|
+
}
|
61
|
+
|
62
|
+
# Treat HTTP 204 No Content as a successful response with empty result
|
63
|
+
if response.status_code == 204:
|
64
|
+
return {
|
65
|
+
"success": True,
|
66
|
+
"result": None,
|
67
|
+
"statusCode": response.status_code,
|
68
|
+
"errors": []
|
69
|
+
}
|
70
|
+
|
71
|
+
# Handle specific known API bugs with better error messages
|
72
|
+
if response.status_code == 500:
|
73
|
+
error_message = data.get("errors", [""])[0] if data.get("errors") else ""
|
74
|
+
|
75
|
+
# API-001: Policies endpoint parameter validation bug
|
76
|
+
if "GET: /api/public/policies route has internal server error" in error_message:
|
77
|
+
raise ValidationError(
|
78
|
+
"Missing required 'organizationIds' filter parameter. "
|
79
|
+
"Please provide organization IDs to filter policies. "
|
80
|
+
"(Note: This is a known API server bug that returns 500 instead of 400)",
|
81
|
+
status_code=400, # What it should be
|
82
|
+
response_data=data
|
83
|
+
)
|
84
|
+
|
85
|
+
# API-002: Auto asset tags update endpoint bug
|
86
|
+
if "auto-asset-tag" in response.url and response.request.method == "PUT":
|
87
|
+
raise ServerError(
|
88
|
+
"Auto asset tag update is currently unavailable due to a server bug. "
|
89
|
+
"Workaround: Delete the existing tag and create a new one with updated values. "
|
90
|
+
"(Note: This is a known API server issue)",
|
91
|
+
status_code=response.status_code,
|
92
|
+
response_data=data
|
93
|
+
)
|
94
|
+
|
95
|
+
# Generic 500 error with detailed message
|
96
|
+
errors = data.get("errors", [f"Server error: {response.status_code}"])
|
97
|
+
error_text = '; '.join(str(e) for e in errors)
|
98
|
+
raise ServerError(
|
99
|
+
f"Server error: {error_text}",
|
100
|
+
status_code=response.status_code,
|
101
|
+
response_data=data
|
102
|
+
)
|
103
|
+
|
104
|
+
elif response.status_code == 400:
|
105
|
+
# Show detailed validation errors instead of generic "HTTP 400"
|
106
|
+
errors = data.get("errors", [f"Bad request: {response.status_code}"])
|
107
|
+
error_text = '; '.join(str(e) for e in errors)
|
108
|
+
raise ValidationError(
|
109
|
+
f"Validation error: {error_text}",
|
110
|
+
status_code=response.status_code,
|
111
|
+
response_data=data
|
112
|
+
)
|
113
|
+
|
114
|
+
elif response.status_code == 422:
|
115
|
+
errors = data.get("errors", ["Validation failed"])
|
116
|
+
# Handle complex error objects (like OSQuery validation errors)
|
117
|
+
if errors and isinstance(errors[0], dict):
|
118
|
+
error_messages = []
|
119
|
+
for error in errors:
|
120
|
+
if isinstance(error, dict):
|
121
|
+
if 'message' in error:
|
122
|
+
error_messages.append(error['message'])
|
123
|
+
elif 'errors' in error and isinstance(error['errors'], list):
|
124
|
+
for nested_error in error['errors']:
|
125
|
+
if isinstance(nested_error, dict) and 'message' in nested_error:
|
126
|
+
error_messages.append(nested_error['message'])
|
127
|
+
else:
|
128
|
+
error_messages.append(str(nested_error))
|
129
|
+
else:
|
130
|
+
error_messages.append(str(error))
|
131
|
+
else:
|
132
|
+
error_messages.append(str(error))
|
133
|
+
error_text = '; '.join(error_messages) if error_messages else "Validation failed"
|
134
|
+
else:
|
135
|
+
error_text = '; '.join(str(e) for e in errors)
|
136
|
+
|
137
|
+
raise ValidationError(
|
138
|
+
f"Validation error: {error_text}",
|
139
|
+
status_code=response.status_code,
|
140
|
+
response_data=data
|
141
|
+
)
|
142
|
+
elif response.status_code == 429:
|
143
|
+
raise RateLimitError(
|
144
|
+
"Rate limit exceeded. Please try again later.",
|
145
|
+
status_code=response.status_code,
|
146
|
+
response_data=data
|
147
|
+
)
|
148
|
+
elif response.status_code >= 500:
|
149
|
+
raise ServerError(
|
150
|
+
f"Server error: {response.status_code}",
|
151
|
+
status_code=response.status_code,
|
152
|
+
response_data=data
|
153
|
+
)
|
154
|
+
elif not response.ok:
|
155
|
+
errors = data.get("errors", [f"HTTP {response.status_code}"])
|
156
|
+
# Handle complex error objects (like OSQuery validation errors)
|
157
|
+
if errors and isinstance(errors[0], dict):
|
158
|
+
error_messages = []
|
159
|
+
for error in errors:
|
160
|
+
if isinstance(error, dict):
|
161
|
+
# Extract meaningful error information from complex objects
|
162
|
+
if 'message' in error:
|
163
|
+
error_messages.append(error['message'])
|
164
|
+
elif 'errors' in error and isinstance(error['errors'], list):
|
165
|
+
# Handle nested error structures (like OSQuery validation)
|
166
|
+
for nested_error in error['errors']:
|
167
|
+
if isinstance(nested_error, dict) and 'message' in nested_error:
|
168
|
+
error_messages.append(nested_error['message'])
|
169
|
+
else:
|
170
|
+
error_messages.append(str(nested_error))
|
171
|
+
else:
|
172
|
+
error_messages.append(str(error))
|
173
|
+
else:
|
174
|
+
error_messages.append(str(error))
|
175
|
+
error_text = '; '.join(error_messages) if error_messages else f"HTTP {response.status_code}"
|
176
|
+
else:
|
177
|
+
# Handle simple string errors with detailed error messages
|
178
|
+
error_text = '; '.join(str(e) for e in errors)
|
179
|
+
|
180
|
+
raise AIRAPIError(
|
181
|
+
f"API error: {error_text}",
|
182
|
+
status_code=response.status_code,
|
183
|
+
response_data=data
|
184
|
+
)
|
185
|
+
|
186
|
+
return data
|
187
|
+
|
188
|
+
def _handle_binary_response(self, response: requests.Response) -> requests.Response:
|
189
|
+
"""Handle binary file response without JSON parsing."""
|
190
|
+
# Check for specific error status codes
|
191
|
+
if response.status_code == 401:
|
192
|
+
raise AuthenticationError(
|
193
|
+
"Authentication failed. Check your API token.",
|
194
|
+
status_code=response.status_code
|
195
|
+
)
|
196
|
+
elif response.status_code == 403:
|
197
|
+
raise AuthorizationError(
|
198
|
+
"Authorization failed. Insufficient permissions.",
|
199
|
+
status_code=response.status_code
|
200
|
+
)
|
201
|
+
elif response.status_code == 404:
|
202
|
+
raise NotFoundError(
|
203
|
+
"Resource not found.",
|
204
|
+
status_code=response.status_code
|
205
|
+
)
|
206
|
+
elif response.status_code == 422:
|
207
|
+
raise ValidationError(
|
208
|
+
"Validation error",
|
209
|
+
status_code=response.status_code
|
210
|
+
)
|
211
|
+
elif response.status_code == 429:
|
212
|
+
raise RateLimitError(
|
213
|
+
"Rate limit exceeded. Please try again later.",
|
214
|
+
status_code=response.status_code
|
215
|
+
)
|
216
|
+
elif response.status_code >= 500:
|
217
|
+
raise ServerError(
|
218
|
+
f"Server error: {response.status_code}",
|
219
|
+
status_code=response.status_code
|
220
|
+
)
|
221
|
+
elif not response.ok:
|
222
|
+
raise AIRAPIError(
|
223
|
+
f"API error: HTTP {response.status_code}",
|
224
|
+
status_code=response.status_code
|
225
|
+
)
|
226
|
+
|
227
|
+
return response
|
228
|
+
|
229
|
+
def _make_request(
|
230
|
+
self,
|
231
|
+
method: str,
|
232
|
+
endpoint: str,
|
233
|
+
params: Optional[Dict[str, Any]] = None,
|
234
|
+
data: Optional[Dict[str, Any]] = None,
|
235
|
+
json_data: Optional[Dict[str, Any]] = None
|
236
|
+
) -> Dict[str, Any]:
|
237
|
+
"""Make HTTP request with retry logic."""
|
238
|
+
url = self._build_url(endpoint)
|
239
|
+
last_exception = None
|
240
|
+
|
241
|
+
for attempt in range(self.config.retry_attempts):
|
242
|
+
try:
|
243
|
+
response = self.session.request(
|
244
|
+
method=method,
|
245
|
+
url=url,
|
246
|
+
params=params,
|
247
|
+
data=data,
|
248
|
+
json=json_data,
|
249
|
+
timeout=self.config.timeout
|
250
|
+
)
|
251
|
+
return self._handle_response(response)
|
252
|
+
|
253
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
254
|
+
last_exception = NetworkError(f"Network error: {str(e)}")
|
255
|
+
if attempt < self.config.retry_attempts - 1:
|
256
|
+
time.sleep(self.config.retry_delay * (2 ** attempt)) # Exponential backoff
|
257
|
+
continue
|
258
|
+
raise last_exception
|
259
|
+
|
260
|
+
except (RateLimitError, ServerError) as e:
|
261
|
+
last_exception = e
|
262
|
+
if attempt < self.config.retry_attempts - 1:
|
263
|
+
time.sleep(self.config.retry_delay * (2 ** attempt))
|
264
|
+
continue
|
265
|
+
raise
|
266
|
+
|
267
|
+
except (AuthenticationError, AuthorizationError, NotFoundError, ValidationError) as e:
|
268
|
+
# Don't retry these errors
|
269
|
+
raise
|
270
|
+
|
271
|
+
# If we get here, all retries failed
|
272
|
+
if last_exception:
|
273
|
+
raise last_exception
|
274
|
+
|
275
|
+
raise AIRAPIError("All retry attempts failed")
|
276
|
+
|
277
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
278
|
+
"""Make GET request."""
|
279
|
+
return self._make_request("GET", endpoint, params=params)
|
280
|
+
|
281
|
+
def post(
|
282
|
+
self,
|
283
|
+
endpoint: str,
|
284
|
+
data: Optional[Dict[str, Any]] = None,
|
285
|
+
json_data: Optional[Dict[str, Any]] = None,
|
286
|
+
params: Optional[Dict[str, Any]] = None
|
287
|
+
) -> Dict[str, Any]:
|
288
|
+
"""Make POST request."""
|
289
|
+
return self._make_request("POST", endpoint, params=params, data=data, json_data=json_data)
|
290
|
+
|
291
|
+
def put(
|
292
|
+
self,
|
293
|
+
endpoint: str,
|
294
|
+
data: Optional[Dict[str, Any]] = None,
|
295
|
+
json_data: Optional[Dict[str, Any]] = None,
|
296
|
+
params: Optional[Dict[str, Any]] = None
|
297
|
+
) -> Dict[str, Any]:
|
298
|
+
"""Make PUT request."""
|
299
|
+
return self._make_request("PUT", endpoint, params=params, data=data, json_data=json_data)
|
300
|
+
|
301
|
+
def patch(
|
302
|
+
self,
|
303
|
+
endpoint: str,
|
304
|
+
data: Optional[Dict[str, Any]] = None,
|
305
|
+
json_data: Optional[Dict[str, Any]] = None,
|
306
|
+
params: Optional[Dict[str, Any]] = None
|
307
|
+
) -> Dict[str, Any]:
|
308
|
+
"""Make PATCH request."""
|
309
|
+
return self._make_request("PATCH", endpoint, params=params, data=data, json_data=json_data)
|
310
|
+
|
311
|
+
def delete(
|
312
|
+
self,
|
313
|
+
endpoint: str,
|
314
|
+
params: Optional[Dict[str, Any]] = None,
|
315
|
+
json_data: Optional[Dict[str, Any]] = None
|
316
|
+
) -> Dict[str, Any]:
|
317
|
+
"""Make DELETE request."""
|
318
|
+
return self._make_request("DELETE", endpoint, params=params, json_data=json_data)
|
319
|
+
|
320
|
+
def get_binary(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> requests.Response:
|
321
|
+
"""Make GET request for binary file downloads."""
|
322
|
+
url = self._build_url(endpoint)
|
323
|
+
last_exception = None
|
324
|
+
|
325
|
+
for attempt in range(self.config.retry_attempts):
|
326
|
+
try:
|
327
|
+
response = self.session.request(
|
328
|
+
method="GET",
|
329
|
+
url=url,
|
330
|
+
params=params,
|
331
|
+
timeout=self.config.timeout
|
332
|
+
)
|
333
|
+
return self._handle_binary_response(response)
|
334
|
+
|
335
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
336
|
+
last_exception = NetworkError(f"Network error: {str(e)}")
|
337
|
+
if attempt < self.config.retry_attempts - 1:
|
338
|
+
time.sleep(self.config.retry_delay * (2 ** attempt)) # Exponential backoff
|
339
|
+
continue
|
340
|
+
raise last_exception
|
341
|
+
|
342
|
+
except (RateLimitError, ServerError) as e:
|
343
|
+
last_exception = e
|
344
|
+
if attempt < self.config.retry_attempts - 1:
|
345
|
+
time.sleep(self.config.retry_delay * (2 ** attempt))
|
346
|
+
continue
|
347
|
+
raise
|
348
|
+
|
349
|
+
except (AuthenticationError, AuthorizationError, NotFoundError, ValidationError) as e:
|
350
|
+
# Don't retry these errors
|
351
|
+
raise
|
352
|
+
|
353
|
+
# If we get here, all retries failed
|
354
|
+
if last_exception:
|
355
|
+
raise last_exception
|
356
|
+
|
357
|
+
raise AIRAPIError("All retry attempts failed")
|
358
|
+
|
359
|
+
def upload_multipart(
|
360
|
+
self,
|
361
|
+
endpoint: str,
|
362
|
+
files: Optional[Dict[str, Any]] = None,
|
363
|
+
data: Optional[Dict[str, Any]] = None,
|
364
|
+
params: Optional[Dict[str, Any]] = None,
|
365
|
+
method: str = "POST"
|
366
|
+
) -> Dict[str, Any]:
|
367
|
+
"""Make multipart file upload request.
|
368
|
+
|
369
|
+
Args:
|
370
|
+
endpoint: API endpoint
|
371
|
+
files: Dictionary with file data for upload
|
372
|
+
data: Form data fields
|
373
|
+
params: Query parameters
|
374
|
+
method: HTTP method (POST or PUT)
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
Parsed JSON response
|
378
|
+
"""
|
379
|
+
url = self._build_url(endpoint)
|
380
|
+
last_exception = None
|
381
|
+
|
382
|
+
for attempt in range(self.config.retry_attempts):
|
383
|
+
try:
|
384
|
+
# Temporarily remove Content-Type from session headers
|
385
|
+
# to let requests library set the appropriate multipart/form-data header
|
386
|
+
original_content_type = self.session.headers.pop('Content-Type', None)
|
387
|
+
|
388
|
+
try:
|
389
|
+
response = self.session.request(
|
390
|
+
method=method,
|
391
|
+
url=url,
|
392
|
+
params=params,
|
393
|
+
data=data,
|
394
|
+
files=files,
|
395
|
+
timeout=self.config.timeout
|
396
|
+
)
|
397
|
+
result = self._handle_response(response)
|
398
|
+
finally:
|
399
|
+
# Restore original Content-Type header
|
400
|
+
if original_content_type:
|
401
|
+
self.session.headers['Content-Type'] = original_content_type
|
402
|
+
|
403
|
+
return result
|
404
|
+
|
405
|
+
except (requests.ConnectionError, requests.Timeout) as e:
|
406
|
+
last_exception = NetworkError(f"Network error: {str(e)}")
|
407
|
+
if attempt < self.config.retry_attempts - 1:
|
408
|
+
time.sleep(self.config.retry_delay * (2 ** attempt)) # Exponential backoff
|
409
|
+
continue
|
410
|
+
raise last_exception
|
411
|
+
|
412
|
+
except (RateLimitError, ServerError) as e:
|
413
|
+
last_exception = e
|
414
|
+
if attempt < self.config.retry_attempts - 1:
|
415
|
+
time.sleep(self.config.retry_delay * (2 ** attempt))
|
416
|
+
continue
|
417
|
+
raise
|
418
|
+
|
419
|
+
except (AuthenticationError, AuthorizationError, NotFoundError, ValidationError) as e:
|
420
|
+
# Don't retry these errors
|
421
|
+
raise
|
422
|
+
|
423
|
+
# If we get here, all retries failed
|
424
|
+
if last_exception:
|
425
|
+
raise last_exception
|
426
|
+
|
306
427
|
raise AIRAPIError("All retry attempts failed")
|