feldera 0.131.0__py3-none-any.whl → 0.192.0__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 feldera might be problematic. Click here for more details.
- feldera/__init__.py +2 -2
- feldera/_callback_runner.py +64 -88
- feldera/_helpers.py +8 -2
- feldera/enums.py +145 -92
- feldera/output_handler.py +16 -4
- feldera/pipeline.py +413 -152
- feldera/pipeline_builder.py +15 -8
- feldera/rest/_helpers.py +32 -1
- feldera/rest/_httprequests.py +365 -219
- feldera/rest/config.py +44 -33
- feldera/rest/errors.py +16 -0
- feldera/rest/feldera_client.py +395 -203
- feldera/rest/pipeline.py +15 -0
- feldera/runtime_config.py +4 -0
- feldera/stats.py +4 -1
- feldera/tests/test_datafusionize.py +38 -0
- feldera/testutils.py +382 -0
- feldera/testutils_oidc.py +368 -0
- feldera-0.192.0.dist-info/METADATA +163 -0
- feldera-0.192.0.dist-info/RECORD +26 -0
- feldera-0.131.0.dist-info/METADATA +0 -102
- feldera-0.131.0.dist-info/RECORD +0 -23
- {feldera-0.131.0.dist-info → feldera-0.192.0.dist-info}/WHEEL +0 -0
- {feldera-0.131.0.dist-info → feldera-0.192.0.dist-info}/top_level.txt +0 -0
feldera/rest/_httprequests.py
CHANGED
|
@@ -1,219 +1,365 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
self.
|
|
26
|
-
self.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Callable, List, Mapping, Optional, Sequence, Union
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests.packages import urllib3
|
|
8
|
+
|
|
9
|
+
from feldera.rest.config import Config
|
|
10
|
+
from feldera.rest.errors import (
|
|
11
|
+
FelderaAPIError,
|
|
12
|
+
FelderaCommunicationError,
|
|
13
|
+
FelderaTimeoutError,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def json_serialize(body: Any) -> str:
|
|
18
|
+
# serialize as string if this object cannot be serialized (e.g. UUID)
|
|
19
|
+
return json.dumps(body, default=str) if body else "" if body == "" else "null"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HttpRequests:
|
|
23
|
+
def __init__(self, config: Config) -> None:
|
|
24
|
+
self.config = config
|
|
25
|
+
self.headers = {"User-Agent": "feldera-python-sdk/v1"}
|
|
26
|
+
self.requests_verify = config.requests_verify
|
|
27
|
+
|
|
28
|
+
if isinstance(self.requests_verify, bool) and not self.requests_verify:
|
|
29
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
30
|
+
|
|
31
|
+
if self.config.api_key:
|
|
32
|
+
self.headers["Authorization"] = f"Bearer {self.config.api_key}"
|
|
33
|
+
|
|
34
|
+
def _check_cluster_health(self) -> bool:
|
|
35
|
+
"""Check cluster health via /cluster_healthz endpoint.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
bool: True if both runner and compiler services are healthy, False otherwise
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
health_path = (
|
|
42
|
+
self.config.url + "/" + self.config.version + "/cluster_healthz"
|
|
43
|
+
)
|
|
44
|
+
response = requests.get(
|
|
45
|
+
health_path,
|
|
46
|
+
timeout=(self.config.connection_timeout, self.config.timeout),
|
|
47
|
+
headers=self.headers,
|
|
48
|
+
verify=self.requests_verify,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if response.status_code == 200:
|
|
52
|
+
health_data = response.json()
|
|
53
|
+
runner_healthy = health_data.get("runner", {}).get("healthy", False)
|
|
54
|
+
compiler_healthy = health_data.get("compiler", {}).get("healthy", False)
|
|
55
|
+
return runner_healthy and compiler_healthy
|
|
56
|
+
else:
|
|
57
|
+
logging.warning(
|
|
58
|
+
f"Health check returned status {response.status_code}. The instance might be in the process of being upgraded. Waiting to see if it recovers."
|
|
59
|
+
)
|
|
60
|
+
return False
|
|
61
|
+
except Exception as e:
|
|
62
|
+
logging.error(f"Health check failed: {e}")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def _wait_for_health_recovery(self, max_wait_seconds: int = 300) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Wait for cluster to become healthy after detecting upgrade/restart.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
max_wait_seconds: Maximum time to wait for health recovery (default 5 minutes)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
bool: True if cluster became healthy within timeout, False otherwise
|
|
74
|
+
"""
|
|
75
|
+
start_time = time.time()
|
|
76
|
+
check_interval = 5
|
|
77
|
+
|
|
78
|
+
logging.info(
|
|
79
|
+
f"Waiting for cluster health recovery (max {max_wait_seconds}s)..."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
while time.time() - start_time < max_wait_seconds:
|
|
83
|
+
if self._check_cluster_health():
|
|
84
|
+
elapsed = time.time() - start_time
|
|
85
|
+
logging.info(f"Instance health recovered after {elapsed:.1f}s")
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
time.sleep(check_interval)
|
|
89
|
+
elapsed = time.time() - start_time
|
|
90
|
+
logging.debug(
|
|
91
|
+
f"Still waiting for health recovery ({elapsed:.1f}s elapsed)..."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
logging.warning(f"Instance did not recover within {max_wait_seconds}s timeout")
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
def _handle_502_with_health_check(
|
|
98
|
+
self, path: str, attempt: int, max_retries: int
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""
|
|
101
|
+
Handles 502 errors with health monitoring.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
path: The request path that failed
|
|
105
|
+
attempt: Current attempt number (0-based)
|
|
106
|
+
max_retries: Maximum number of retries allowed
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
bool: True if should retry, False if should give up
|
|
110
|
+
"""
|
|
111
|
+
if attempt >= max_retries:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
logging.warning(
|
|
115
|
+
f"HTTP 502 received for {path} (attempt {attempt + 1}/{max_retries + 1}), checking cluster health..."
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Do a short backoff and retry
|
|
119
|
+
time.sleep(min(2 << attempt, 64))
|
|
120
|
+
|
|
121
|
+
# First, check if cluster is currently healthy
|
|
122
|
+
if self._check_cluster_health():
|
|
123
|
+
# Instance appears healthy, this might be a spurious 502
|
|
124
|
+
logging.info(
|
|
125
|
+
f"Instance appears healthy, treating 502 for {path} as spurious - retrying"
|
|
126
|
+
)
|
|
127
|
+
return True
|
|
128
|
+
else:
|
|
129
|
+
# Instance is unhealthy, likely an upgrade/restart scenario
|
|
130
|
+
logging.info(
|
|
131
|
+
f"Instance unhealthy for request to {path}, likely upgrade in progress - waiting for recovery..."
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Wait for cluster to recover (up to 5 minutes)
|
|
135
|
+
recovery_timeout = self.config.health_recovery_timeout
|
|
136
|
+
if self._wait_for_health_recovery(max_wait_seconds=recovery_timeout):
|
|
137
|
+
logging.info(f"Instance health recovered, can now retry {path}...")
|
|
138
|
+
return True
|
|
139
|
+
else:
|
|
140
|
+
# Health didn't recover within timeout
|
|
141
|
+
logging.error(
|
|
142
|
+
f"Instance health did not recover within {recovery_timeout}s timeout for {path}, giving up"
|
|
143
|
+
)
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def send_request(
|
|
147
|
+
self,
|
|
148
|
+
http_method: Callable,
|
|
149
|
+
path: str,
|
|
150
|
+
body: Optional[
|
|
151
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
152
|
+
] = None,
|
|
153
|
+
content_type: str = "application/json",
|
|
154
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
155
|
+
stream: bool = False,
|
|
156
|
+
serialize: bool = True,
|
|
157
|
+
max_retries: int = 3,
|
|
158
|
+
) -> Any:
|
|
159
|
+
"""
|
|
160
|
+
Send HTTP request with intelligent retry logic.
|
|
161
|
+
|
|
162
|
+
Handles different error conditions with appropriate retry strategies:
|
|
163
|
+
- 502 errors: Checks cluster health and waits for recovery during upgrades
|
|
164
|
+
- 503/408 errors: Standard retry with backoff
|
|
165
|
+
- Timeout errors: Standard retry with backoff
|
|
166
|
+
|
|
167
|
+
For 502 errors, the method distinguishes between:
|
|
168
|
+
1. Spurious 502s (cluster healthy): Quick retry with short backoff
|
|
169
|
+
2. Upgrade scenarios (cluster unhealthy): Waits up to health_recovery_timeout
|
|
170
|
+
for cluster to become healthy, then retries
|
|
171
|
+
|
|
172
|
+
:param http_method: The HTTP method to use. Takes the equivalent `requests.*` module. (Example: `requests.get`)
|
|
173
|
+
:param path: The path to send the request to.
|
|
174
|
+
:param body: The HTTP request body.
|
|
175
|
+
:param content_type: The value for `Content-Type` HTTP header. "application/json" by default.
|
|
176
|
+
:param params: The query parameters part of this request.
|
|
177
|
+
:param stream: True if the response is expected to be a HTTP stream.
|
|
178
|
+
:param serialize: True if the body needs to be serialized to JSON.
|
|
179
|
+
:param max_retries: Maximum number of retry attempts.
|
|
180
|
+
"""
|
|
181
|
+
self.headers["Content-Type"] = content_type
|
|
182
|
+
|
|
183
|
+
prev_resp: requests.Response | None = None
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
conn_timeout = self.config.connection_timeout
|
|
187
|
+
timeout = self.config.timeout
|
|
188
|
+
headers = self.headers
|
|
189
|
+
|
|
190
|
+
request_path = self.config.url + "/" + self.config.version + path
|
|
191
|
+
|
|
192
|
+
logging.debug(
|
|
193
|
+
"sending %s request to: %s with headers: %s, and params: %s",
|
|
194
|
+
http_method.__name__,
|
|
195
|
+
request_path,
|
|
196
|
+
str(headers),
|
|
197
|
+
str(params),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
for attempt in range(max_retries):
|
|
201
|
+
if http_method.__name__ == "get":
|
|
202
|
+
request = http_method(
|
|
203
|
+
request_path,
|
|
204
|
+
timeout=(conn_timeout, timeout),
|
|
205
|
+
headers=headers,
|
|
206
|
+
params=params,
|
|
207
|
+
stream=stream,
|
|
208
|
+
verify=self.requests_verify,
|
|
209
|
+
)
|
|
210
|
+
elif isinstance(body, bytes):
|
|
211
|
+
request = http_method(
|
|
212
|
+
request_path,
|
|
213
|
+
timeout=(conn_timeout, timeout),
|
|
214
|
+
headers=headers,
|
|
215
|
+
data=body,
|
|
216
|
+
params=params,
|
|
217
|
+
stream=stream,
|
|
218
|
+
verify=self.requests_verify,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
request = http_method(
|
|
222
|
+
request_path,
|
|
223
|
+
timeout=(conn_timeout, timeout),
|
|
224
|
+
headers=headers,
|
|
225
|
+
data=json_serialize(body) if serialize else body,
|
|
226
|
+
params=params,
|
|
227
|
+
stream=stream,
|
|
228
|
+
verify=self.requests_verify,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
prev_resp = request
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
resp = self.__validate(request, stream=stream)
|
|
235
|
+
logging.debug("got response: %s", str(resp))
|
|
236
|
+
return resp
|
|
237
|
+
except FelderaAPIError as err:
|
|
238
|
+
# Handle 502 with intelligent health monitoring
|
|
239
|
+
if err.status_code == 502:
|
|
240
|
+
if self._handle_502_with_health_check(
|
|
241
|
+
path, attempt, max_retries
|
|
242
|
+
):
|
|
243
|
+
continue
|
|
244
|
+
else:
|
|
245
|
+
raise
|
|
246
|
+
# Handle other retryable errors
|
|
247
|
+
elif err.status_code in [408, 503, 504]:
|
|
248
|
+
if attempt < max_retries:
|
|
249
|
+
logging.warning(
|
|
250
|
+
"HTTP %d received for %s, retrying (%d/%d)...",
|
|
251
|
+
err.status_code,
|
|
252
|
+
path,
|
|
253
|
+
attempt + 1,
|
|
254
|
+
max_retries,
|
|
255
|
+
)
|
|
256
|
+
time.sleep(min(2 << attempt, 64))
|
|
257
|
+
continue
|
|
258
|
+
raise # re-raise for all other errors or if out of retries
|
|
259
|
+
except requests.exceptions.Timeout as err:
|
|
260
|
+
if attempt < max_retries:
|
|
261
|
+
logging.warning(
|
|
262
|
+
"HTTP Connection Timeout for %s, retrying (%d/%d)...",
|
|
263
|
+
path,
|
|
264
|
+
attempt + 1,
|
|
265
|
+
max_retries,
|
|
266
|
+
)
|
|
267
|
+
time.sleep(2)
|
|
268
|
+
continue
|
|
269
|
+
raise FelderaTimeoutError(str(err)) from err
|
|
270
|
+
|
|
271
|
+
except requests.exceptions.ConnectionError as err:
|
|
272
|
+
raise FelderaCommunicationError(str(err)) from err
|
|
273
|
+
|
|
274
|
+
raise FelderaAPIError(
|
|
275
|
+
"Max retries exceeded, couldn't successfully connect to Feldera", prev_resp
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def get(
|
|
279
|
+
self,
|
|
280
|
+
path: str,
|
|
281
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
282
|
+
stream: bool = False,
|
|
283
|
+
) -> Any:
|
|
284
|
+
return self.send_request(requests.get, path, params=params, stream=stream)
|
|
285
|
+
|
|
286
|
+
def post(
|
|
287
|
+
self,
|
|
288
|
+
path: str,
|
|
289
|
+
body: Optional[
|
|
290
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
291
|
+
] = None,
|
|
292
|
+
content_type: str = "application/json",
|
|
293
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
294
|
+
stream: bool = False,
|
|
295
|
+
serialize: bool = True,
|
|
296
|
+
) -> Any:
|
|
297
|
+
return self.send_request(
|
|
298
|
+
requests.post,
|
|
299
|
+
path,
|
|
300
|
+
body,
|
|
301
|
+
content_type,
|
|
302
|
+
params,
|
|
303
|
+
stream=stream,
|
|
304
|
+
serialize=serialize,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def patch(
|
|
308
|
+
self,
|
|
309
|
+
path: str,
|
|
310
|
+
body: Optional[
|
|
311
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
312
|
+
] = None,
|
|
313
|
+
content_type: str = "application/json",
|
|
314
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
315
|
+
) -> Any:
|
|
316
|
+
return self.send_request(requests.patch, path, body, content_type, params)
|
|
317
|
+
|
|
318
|
+
def put(
|
|
319
|
+
self,
|
|
320
|
+
path: str,
|
|
321
|
+
body: Optional[
|
|
322
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str], str]
|
|
323
|
+
] = None,
|
|
324
|
+
content_type: str = "application/json",
|
|
325
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
326
|
+
) -> Any:
|
|
327
|
+
return self.send_request(requests.put, path, body, content_type, params)
|
|
328
|
+
|
|
329
|
+
def delete(
|
|
330
|
+
self,
|
|
331
|
+
path: str,
|
|
332
|
+
body: Optional[
|
|
333
|
+
Union[Mapping[str, Any], Sequence[Mapping[str, Any]], List[str]]
|
|
334
|
+
] = None,
|
|
335
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
336
|
+
) -> Any:
|
|
337
|
+
return self.send_request(requests.delete, path, body, params=params)
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def __to_json(request: requests.Response) -> Any:
|
|
341
|
+
if request.content == b"":
|
|
342
|
+
return request
|
|
343
|
+
return request.json()
|
|
344
|
+
|
|
345
|
+
@staticmethod
|
|
346
|
+
def __validate(request: requests.Response, stream=False) -> Any:
|
|
347
|
+
try:
|
|
348
|
+
request.raise_for_status()
|
|
349
|
+
|
|
350
|
+
if request is None:
|
|
351
|
+
# This shouldn't ever be the case, but we've seen it happen
|
|
352
|
+
return FelderaCommunicationError(
|
|
353
|
+
"Failed to Communicate with Feldera Received None as Response",
|
|
354
|
+
)
|
|
355
|
+
if stream:
|
|
356
|
+
return request
|
|
357
|
+
if request.headers.get("content-type") == "text/plain":
|
|
358
|
+
return request.text
|
|
359
|
+
elif request.headers.get("content-type") == "application/octet-stream":
|
|
360
|
+
return request.content
|
|
361
|
+
|
|
362
|
+
resp = HttpRequests.__to_json(request)
|
|
363
|
+
return resp
|
|
364
|
+
except requests.exceptions.HTTPError as err:
|
|
365
|
+
raise FelderaAPIError(str(err), request) from err
|