supervisely 6.73.221__py3-none-any.whl → 6.73.222__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 supervisely might be problematic. Click here for more details.
- supervisely/api/api.py +609 -3
- supervisely/api/file_api.py +522 -9
- supervisely/api/image_api.py +469 -0
- supervisely/api/pointcloud/pointcloud_api.py +390 -1
- supervisely/api/video/video_api.py +231 -1
- supervisely/api/volume/volume_api.py +223 -2
- supervisely/convert/base_converter.py +44 -3
- supervisely/convert/converter.py +6 -5
- supervisely/convert/image/image_converter.py +26 -13
- supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
- supervisely/convert/image/sly/sly_image_converter.py +9 -4
- supervisely/convert/video/sly/sly_video_converter.py +9 -1
- supervisely/convert/video/video_converter.py +44 -23
- supervisely/io/fs.py +125 -0
- supervisely/io/fs_cache.py +19 -1
- supervisely/io/network_exceptions.py +20 -3
- supervisely/task/progress.py +1 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/METADATA +3 -1
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/RECORD +23 -23
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/LICENSE +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/WHEEL +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/entry_points.txt +0 -0
- {supervisely-6.73.221.dist-info → supervisely-6.73.222.dist-info}/top_level.txt +0 -0
supervisely/api/api.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
|
+
import asyncio
|
|
6
7
|
import datetime
|
|
7
8
|
import gc
|
|
8
9
|
import glob
|
|
@@ -11,9 +12,21 @@ import os
|
|
|
11
12
|
import shutil
|
|
12
13
|
from logging import Logger
|
|
13
14
|
from pathlib import Path
|
|
14
|
-
from typing import
|
|
15
|
+
from typing import (
|
|
16
|
+
Any,
|
|
17
|
+
AsyncGenerator,
|
|
18
|
+
AsyncIterable,
|
|
19
|
+
Dict,
|
|
20
|
+
Generator,
|
|
21
|
+
Iterable,
|
|
22
|
+
Literal,
|
|
23
|
+
Mapping,
|
|
24
|
+
Optional,
|
|
25
|
+
Union,
|
|
26
|
+
)
|
|
15
27
|
from urllib.parse import urljoin, urlparse
|
|
16
28
|
|
|
29
|
+
import httpx
|
|
17
30
|
import jwt
|
|
18
31
|
import requests
|
|
19
32
|
from dotenv import get_key, load_dotenv, set_key
|
|
@@ -362,6 +375,9 @@ class Api:
|
|
|
362
375
|
if check_instance_version:
|
|
363
376
|
self._check_version(None if check_instance_version is True else check_instance_version)
|
|
364
377
|
|
|
378
|
+
self.async_httpx_client: httpx.AsyncClient = None
|
|
379
|
+
self.httpx_client: httpx.Client = None
|
|
380
|
+
|
|
365
381
|
@classmethod
|
|
366
382
|
def normalize_server_address(cls, server_address: str) -> str:
|
|
367
383
|
""" """
|
|
@@ -742,7 +758,7 @@ class Api:
|
|
|
742
758
|
process_unhandled_request(self.logger, exc)
|
|
743
759
|
|
|
744
760
|
@staticmethod
|
|
745
|
-
def _raise_for_status(response):
|
|
761
|
+
def _raise_for_status(response: requests.Response):
|
|
746
762
|
"""
|
|
747
763
|
Raise error and show message with error code if given response can not connect to server.
|
|
748
764
|
:param response: Request class object
|
|
@@ -775,6 +791,46 @@ class Api:
|
|
|
775
791
|
if http_error_msg:
|
|
776
792
|
raise requests.exceptions.HTTPError(http_error_msg, response=response)
|
|
777
793
|
|
|
794
|
+
@staticmethod
|
|
795
|
+
def _raise_for_status_httpx(response: httpx.Response):
|
|
796
|
+
"""
|
|
797
|
+
Raise error and show message with error code if given response can not connect to server.
|
|
798
|
+
:param response: Response class object
|
|
799
|
+
"""
|
|
800
|
+
http_error_msg = ""
|
|
801
|
+
|
|
802
|
+
if hasattr(response, "reason_phrase"):
|
|
803
|
+
reason = response.reason_phrase
|
|
804
|
+
else:
|
|
805
|
+
reason = "Can't get reason"
|
|
806
|
+
|
|
807
|
+
def decode_response_content(response: httpx.Response):
|
|
808
|
+
if hasattr(response, "is_stream_consumed"):
|
|
809
|
+
return "Content is not acessible for streaming responses"
|
|
810
|
+
else:
|
|
811
|
+
return response.content.decode("utf-8")
|
|
812
|
+
|
|
813
|
+
if 400 <= response.status_code < 500:
|
|
814
|
+
http_error_msg = "%s Client Error: %s for url: %s (%s)" % (
|
|
815
|
+
response.status_code,
|
|
816
|
+
reason,
|
|
817
|
+
response.url,
|
|
818
|
+
decode_response_content(response),
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
elif 500 <= response.status_code < 600:
|
|
822
|
+
http_error_msg = "%s Server Error: %s for url: %s (%s)" % (
|
|
823
|
+
response.status_code,
|
|
824
|
+
reason,
|
|
825
|
+
response.url,
|
|
826
|
+
decode_response_content(response),
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
if http_error_msg:
|
|
830
|
+
raise httpx.HTTPStatusError(
|
|
831
|
+
message=http_error_msg, response=response, request=response.request
|
|
832
|
+
)
|
|
833
|
+
|
|
778
834
|
@staticmethod
|
|
779
835
|
def parse_error(
|
|
780
836
|
response: requests.Response,
|
|
@@ -832,7 +888,7 @@ class Api:
|
|
|
832
888
|
"Supervisely automatically changed the server address to HTTPS for you. "
|
|
833
889
|
f"Consider updating your server address to {self.server_address}"
|
|
834
890
|
)
|
|
835
|
-
self.logger.
|
|
891
|
+
self.logger.warning(msg)
|
|
836
892
|
except:
|
|
837
893
|
pass
|
|
838
894
|
finally:
|
|
@@ -939,3 +995,553 @@ class Api:
|
|
|
939
995
|
return self._api_server_address
|
|
940
996
|
|
|
941
997
|
return f"{self.server_address}/public/api"
|
|
998
|
+
|
|
999
|
+
def post_httpx(
|
|
1000
|
+
self,
|
|
1001
|
+
method: str,
|
|
1002
|
+
data: Union[bytes, Dict],
|
|
1003
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1004
|
+
retries: Optional[int] = None,
|
|
1005
|
+
raise_error: Optional[bool] = False,
|
|
1006
|
+
) -> httpx.Response:
|
|
1007
|
+
"""
|
|
1008
|
+
Performs POST request to server with given parameters using httpx.
|
|
1009
|
+
|
|
1010
|
+
:param method: Method name.
|
|
1011
|
+
:type method: str
|
|
1012
|
+
:param data: Bytes with data content or dictionary with params.
|
|
1013
|
+
:type data: bytes or dict
|
|
1014
|
+
:param headers: Custom headers to include in the request.
|
|
1015
|
+
:type headers: dict, optional
|
|
1016
|
+
:param retries: The number of attempts to connect to the server.
|
|
1017
|
+
:type retries: int, optional
|
|
1018
|
+
:param raise_error: Define, if you'd like to raise error if connection is failed.
|
|
1019
|
+
:type raise_error: bool, optional
|
|
1020
|
+
:return: Response object
|
|
1021
|
+
:rtype: :class:`httpx.Response`
|
|
1022
|
+
"""
|
|
1023
|
+
self._set_client()
|
|
1024
|
+
if retries is None:
|
|
1025
|
+
retries = self.retry_count
|
|
1026
|
+
|
|
1027
|
+
url = self.api_server_address + "/v3/" + method
|
|
1028
|
+
logger.trace(f"POST {url}")
|
|
1029
|
+
|
|
1030
|
+
if headers is None:
|
|
1031
|
+
headers = self.headers.copy()
|
|
1032
|
+
else:
|
|
1033
|
+
headers = {**self.headers, **headers}
|
|
1034
|
+
|
|
1035
|
+
if isinstance(data, bytes):
|
|
1036
|
+
request_params = {"content": data}
|
|
1037
|
+
elif isinstance(data, Dict):
|
|
1038
|
+
json_body = {**data, **self.additional_fields}
|
|
1039
|
+
request_params = {"json": json_body}
|
|
1040
|
+
else:
|
|
1041
|
+
request_params = {"params": data}
|
|
1042
|
+
|
|
1043
|
+
for retry_idx in range(retries):
|
|
1044
|
+
response = None
|
|
1045
|
+
try:
|
|
1046
|
+
response = self.httpx_client.post(url, headers=headers, **request_params)
|
|
1047
|
+
if response.status_code != httpx.codes.OK:
|
|
1048
|
+
self._check_version()
|
|
1049
|
+
Api._raise_for_status_httpx(response)
|
|
1050
|
+
return response
|
|
1051
|
+
except httpx.RequestError as exc:
|
|
1052
|
+
if raise_error:
|
|
1053
|
+
raise exc
|
|
1054
|
+
else:
|
|
1055
|
+
process_requests_exception(
|
|
1056
|
+
self.logger,
|
|
1057
|
+
exc,
|
|
1058
|
+
method,
|
|
1059
|
+
url,
|
|
1060
|
+
verbose=True,
|
|
1061
|
+
swallow_exc=True,
|
|
1062
|
+
sleep_sec=min(self.retry_sleep_sec * (2**retry_idx), 60),
|
|
1063
|
+
response=response,
|
|
1064
|
+
retry_info={"retry_idx": retry_idx + 1, "retry_limit": retries},
|
|
1065
|
+
)
|
|
1066
|
+
except Exception as exc:
|
|
1067
|
+
process_unhandled_request(self.logger, exc)
|
|
1068
|
+
raise httpx.HTTPStatusError(
|
|
1069
|
+
"Retry limit exceeded ({!r})".format(url),
|
|
1070
|
+
response=response,
|
|
1071
|
+
request=getattr(response, "request", None),
|
|
1072
|
+
)
|
|
1073
|
+
|
|
1074
|
+
def get_httpx(
|
|
1075
|
+
self,
|
|
1076
|
+
method: str,
|
|
1077
|
+
params: httpx._types.QueryParamTypes,
|
|
1078
|
+
retries: Optional[int] = None,
|
|
1079
|
+
use_public_api: Optional[bool] = True,
|
|
1080
|
+
) -> httpx.Response:
|
|
1081
|
+
"""
|
|
1082
|
+
Performs GET request to server with given parameters.
|
|
1083
|
+
|
|
1084
|
+
:param method: Method name.
|
|
1085
|
+
:type method: str
|
|
1086
|
+
:param params: URL query parameters.
|
|
1087
|
+
:type params: httpx._types.QueryParamTypes
|
|
1088
|
+
:param retries: The number of attempts to connect to the server.
|
|
1089
|
+
:type retries: int, optional
|
|
1090
|
+
:param use_public_api: Define if public API should be used. Default is True.
|
|
1091
|
+
:type use_public_api: bool, optional
|
|
1092
|
+
:return: Response object
|
|
1093
|
+
:rtype: :class:`Response<Response>`
|
|
1094
|
+
"""
|
|
1095
|
+
self._set_client()
|
|
1096
|
+
if retries is None:
|
|
1097
|
+
retries = self.retry_count
|
|
1098
|
+
|
|
1099
|
+
url = self.api_server_address + "/v3/" + method
|
|
1100
|
+
if use_public_api is False:
|
|
1101
|
+
url = os.path.join(self.server_address, method)
|
|
1102
|
+
logger.trace(f"GET {url}")
|
|
1103
|
+
|
|
1104
|
+
if isinstance(params, Dict):
|
|
1105
|
+
request_params = {**params, **self.additional_fields}
|
|
1106
|
+
else:
|
|
1107
|
+
request_params = params
|
|
1108
|
+
|
|
1109
|
+
for retry_idx in range(retries):
|
|
1110
|
+
response = None
|
|
1111
|
+
try:
|
|
1112
|
+
response = self.httpx_client.get(url, params=request_params, headers=self.headers)
|
|
1113
|
+
if response.status_code != httpx.codes.OK:
|
|
1114
|
+
Api._raise_for_status_httpx(response)
|
|
1115
|
+
return response
|
|
1116
|
+
except httpx.RequestError as exc:
|
|
1117
|
+
process_requests_exception(
|
|
1118
|
+
self.logger,
|
|
1119
|
+
exc,
|
|
1120
|
+
method,
|
|
1121
|
+
url,
|
|
1122
|
+
verbose=True,
|
|
1123
|
+
swallow_exc=True,
|
|
1124
|
+
sleep_sec=min(self.retry_sleep_sec * (2**retry_idx), 60),
|
|
1125
|
+
response=response,
|
|
1126
|
+
retry_info={"retry_idx": retry_idx + 2, "retry_limit": retries},
|
|
1127
|
+
)
|
|
1128
|
+
except Exception as exc:
|
|
1129
|
+
process_unhandled_request(self.logger, exc)
|
|
1130
|
+
|
|
1131
|
+
def stream(
|
|
1132
|
+
self,
|
|
1133
|
+
method: str,
|
|
1134
|
+
method_type: Literal["GET", "POST"],
|
|
1135
|
+
data: Union[bytes, Dict],
|
|
1136
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1137
|
+
retries: Optional[int] = None,
|
|
1138
|
+
range_start: Optional[int] = None,
|
|
1139
|
+
range_end: Optional[int] = None,
|
|
1140
|
+
raise_error: Optional[bool] = False,
|
|
1141
|
+
chunk_size: int = 8192,
|
|
1142
|
+
use_public_api: Optional[bool] = True,
|
|
1143
|
+
timeout: httpx._types.TimeoutTypes = 15,
|
|
1144
|
+
) -> Generator:
|
|
1145
|
+
"""
|
|
1146
|
+
Performs streaming GET or POST request to server with given parameters.
|
|
1147
|
+
Multipart is not supported.
|
|
1148
|
+
|
|
1149
|
+
:param method: Method name for the request.
|
|
1150
|
+
:type method: str
|
|
1151
|
+
:param method_type: Request type ('GET' or 'POST').
|
|
1152
|
+
:type method_type: str
|
|
1153
|
+
:param data: Bytes with data content or dictionary with params.
|
|
1154
|
+
:type data: bytes or dict
|
|
1155
|
+
:param headers: Custom headers to include in the request.
|
|
1156
|
+
:type headers: dict, optional
|
|
1157
|
+
:param retries: The number of retry attempts.
|
|
1158
|
+
:type retries: int, optional
|
|
1159
|
+
:param range_start: Start byte position for streaming.
|
|
1160
|
+
:type range_start: int, optional
|
|
1161
|
+
:param range_end: End byte position for streaming.
|
|
1162
|
+
:type range_end: int, optional
|
|
1163
|
+
:param raise_error: If True, raise raw error if the request fails.
|
|
1164
|
+
:type raise_error: bool, optional
|
|
1165
|
+
:param chunk_size: Size of the chunks to stream.
|
|
1166
|
+
:type chunk_size: int, optional
|
|
1167
|
+
:param use_public_api: Define if public API should be used.
|
|
1168
|
+
:type use_public_api: bool, optional
|
|
1169
|
+
:param timeout: Overall timeout for the request.
|
|
1170
|
+
:type timeout: float, optional
|
|
1171
|
+
:return: Generator object.
|
|
1172
|
+
:rtype: :class:`Generator`
|
|
1173
|
+
"""
|
|
1174
|
+
|
|
1175
|
+
self._set_client()
|
|
1176
|
+
if retries is None:
|
|
1177
|
+
retries = self.retry_count
|
|
1178
|
+
|
|
1179
|
+
url = self.api_server_address + "/v3/" + method
|
|
1180
|
+
if not use_public_api:
|
|
1181
|
+
url = os.path.join(self.server_address, method)
|
|
1182
|
+
|
|
1183
|
+
if headers is None:
|
|
1184
|
+
headers = self.headers.copy()
|
|
1185
|
+
else:
|
|
1186
|
+
headers = {**self.headers, **headers}
|
|
1187
|
+
|
|
1188
|
+
logger.trace(f"{method_type} {url}")
|
|
1189
|
+
|
|
1190
|
+
if isinstance(data, (bytes, Generator)):
|
|
1191
|
+
content = data
|
|
1192
|
+
json_body = None
|
|
1193
|
+
params = None
|
|
1194
|
+
elif isinstance(data, Dict):
|
|
1195
|
+
json_body = {**data, **self.additional_fields}
|
|
1196
|
+
content = None
|
|
1197
|
+
params = None
|
|
1198
|
+
else:
|
|
1199
|
+
params = data
|
|
1200
|
+
content = None
|
|
1201
|
+
json_body = None
|
|
1202
|
+
|
|
1203
|
+
if range_start is not None or range_end is not None:
|
|
1204
|
+
headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
|
|
1205
|
+
logger.debug(f"Setting Range header: {headers['Range']}")
|
|
1206
|
+
|
|
1207
|
+
for retry_idx in range(retries):
|
|
1208
|
+
total_streamed = 0
|
|
1209
|
+
try:
|
|
1210
|
+
if method_type == "POST":
|
|
1211
|
+
response = self.httpx_client.stream(
|
|
1212
|
+
method_type,
|
|
1213
|
+
url,
|
|
1214
|
+
content=content,
|
|
1215
|
+
json=json_body,
|
|
1216
|
+
params=params,
|
|
1217
|
+
headers=headers,
|
|
1218
|
+
timeout=timeout,
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
elif method_type == "GET":
|
|
1222
|
+
response = self.httpx_client.stream(
|
|
1223
|
+
method_type,
|
|
1224
|
+
url,
|
|
1225
|
+
params=json_body or params,
|
|
1226
|
+
headers=headers,
|
|
1227
|
+
timeout=timeout,
|
|
1228
|
+
)
|
|
1229
|
+
else:
|
|
1230
|
+
raise NotImplementedError(
|
|
1231
|
+
f"Unsupported method type: {method_type}. Supported types: 'GET', 'POST'"
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
with response as resp:
|
|
1235
|
+
expected_size = int(resp.headers.get("content-length", 0))
|
|
1236
|
+
if resp.status_code not in [
|
|
1237
|
+
httpx.codes.OK,
|
|
1238
|
+
httpx.codes.PARTIAL_CONTENT,
|
|
1239
|
+
]:
|
|
1240
|
+
self._check_version()
|
|
1241
|
+
Api._raise_for_status_httpx(resp)
|
|
1242
|
+
|
|
1243
|
+
hhash = resp.headers.get("x-content-checksum-sha256", None)
|
|
1244
|
+
for chunk in resp.iter_raw(chunk_size):
|
|
1245
|
+
yield chunk, hhash
|
|
1246
|
+
total_streamed += len(chunk)
|
|
1247
|
+
if expected_size != 0 and total_streamed != expected_size:
|
|
1248
|
+
raise ValueError(
|
|
1249
|
+
f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
|
|
1250
|
+
)
|
|
1251
|
+
logger.debug(f"Streamed size: {total_streamed}, expected size: {expected_size}")
|
|
1252
|
+
return
|
|
1253
|
+
except httpx.RequestError as e:
|
|
1254
|
+
retry_range_start = total_streamed + (range_start or 0)
|
|
1255
|
+
if total_streamed != 0:
|
|
1256
|
+
retry_range_start += 1
|
|
1257
|
+
headers["Range"] = f"bytes={retry_range_start}-{range_end or ''}"
|
|
1258
|
+
logger.debug(f"Setting Range header {headers['Range']} for retry")
|
|
1259
|
+
if raise_error:
|
|
1260
|
+
raise e
|
|
1261
|
+
else:
|
|
1262
|
+
process_requests_exception(
|
|
1263
|
+
self.logger,
|
|
1264
|
+
e,
|
|
1265
|
+
method,
|
|
1266
|
+
url,
|
|
1267
|
+
verbose=True,
|
|
1268
|
+
swallow_exc=True,
|
|
1269
|
+
sleep_sec=min(self.retry_sleep_sec * (2**retry_idx), 60),
|
|
1270
|
+
response=locals().get("resp"),
|
|
1271
|
+
retry_info={"retry_idx": retry_idx + 1, "retry_limit": retries},
|
|
1272
|
+
)
|
|
1273
|
+
except Exception as e:
|
|
1274
|
+
process_unhandled_request(self.logger, e)
|
|
1275
|
+
raise httpx.RequestError(
|
|
1276
|
+
message=f"Retry limit exceeded ({url})",
|
|
1277
|
+
request=resp.request if locals().get("resp") else None,
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
async def post_async(
|
|
1281
|
+
self,
|
|
1282
|
+
method: str,
|
|
1283
|
+
json: Dict = None,
|
|
1284
|
+
content: Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] = None,
|
|
1285
|
+
files: Union[Mapping] = None,
|
|
1286
|
+
params: Union[str, bytes] = None,
|
|
1287
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1288
|
+
retries: Optional[int] = None,
|
|
1289
|
+
raise_error: Optional[bool] = False,
|
|
1290
|
+
) -> httpx.Response:
|
|
1291
|
+
"""
|
|
1292
|
+
Performs POST request to server with given parameters using httpx.
|
|
1293
|
+
|
|
1294
|
+
:param method: Method name.
|
|
1295
|
+
:type method: str
|
|
1296
|
+
:param json: Dictionary to send in the body of request.
|
|
1297
|
+
:type json: dict, optional
|
|
1298
|
+
:param content: Bytes with data content or dictionary with params.
|
|
1299
|
+
:type content: bytes or dict, optional
|
|
1300
|
+
:param files: Files to send in the body of request.
|
|
1301
|
+
:type files: dict, optional
|
|
1302
|
+
:param params: URL query parameters.
|
|
1303
|
+
:type params: str, bytes, optional
|
|
1304
|
+
:param headers: Custom headers to include in the request.
|
|
1305
|
+
:type headers: dict, optional
|
|
1306
|
+
:param retries: The number of attempts to connect to the server.
|
|
1307
|
+
:type retries: int, optional
|
|
1308
|
+
:param raise_error: Define, if you'd like to raise error if connection is failed.
|
|
1309
|
+
:type raise_error: bool, optional
|
|
1310
|
+
:return: Response object
|
|
1311
|
+
:rtype: :class:`httpx.Response`
|
|
1312
|
+
"""
|
|
1313
|
+
self._set_async_client()
|
|
1314
|
+
|
|
1315
|
+
if retries is None:
|
|
1316
|
+
retries = self.retry_count
|
|
1317
|
+
|
|
1318
|
+
url = self.api_server_address + "/v3/" + method
|
|
1319
|
+
logger.trace(f"POST {url}")
|
|
1320
|
+
|
|
1321
|
+
if headers is None:
|
|
1322
|
+
headers = self.headers.copy()
|
|
1323
|
+
else:
|
|
1324
|
+
headers = {**self.headers, **headers}
|
|
1325
|
+
|
|
1326
|
+
for retry_idx in range(retries):
|
|
1327
|
+
response = None
|
|
1328
|
+
try:
|
|
1329
|
+
response = await self.async_httpx_client.post(
|
|
1330
|
+
url,
|
|
1331
|
+
content=content,
|
|
1332
|
+
files=files,
|
|
1333
|
+
json=json,
|
|
1334
|
+
params=params,
|
|
1335
|
+
headers=headers,
|
|
1336
|
+
)
|
|
1337
|
+
if response.status_code != httpx.codes.OK:
|
|
1338
|
+
self._check_version()
|
|
1339
|
+
Api._raise_for_status_httpx(response)
|
|
1340
|
+
return response
|
|
1341
|
+
except httpx.RequestError as exc:
|
|
1342
|
+
if raise_error:
|
|
1343
|
+
raise exc
|
|
1344
|
+
else:
|
|
1345
|
+
process_requests_exception(
|
|
1346
|
+
self.logger,
|
|
1347
|
+
exc,
|
|
1348
|
+
method,
|
|
1349
|
+
url,
|
|
1350
|
+
verbose=True,
|
|
1351
|
+
swallow_exc=True,
|
|
1352
|
+
sleep_sec=min(self.retry_sleep_sec * (2**retry_idx), 60),
|
|
1353
|
+
response=response,
|
|
1354
|
+
retry_info={"retry_idx": retry_idx + 1, "retry_limit": retries},
|
|
1355
|
+
)
|
|
1356
|
+
except Exception as exc:
|
|
1357
|
+
process_unhandled_request(self.logger, exc)
|
|
1358
|
+
raise httpx.RequestError(
|
|
1359
|
+
f"Retry limit exceeded ({url})",
|
|
1360
|
+
request=getattr(response, "request", None),
|
|
1361
|
+
)
|
|
1362
|
+
|
|
1363
|
+
async def stream_async(
|
|
1364
|
+
self,
|
|
1365
|
+
method: str,
|
|
1366
|
+
method_type: Literal["GET", "POST"],
|
|
1367
|
+
data: Union[bytes, Dict],
|
|
1368
|
+
headers: Optional[Dict[str, str]] = None,
|
|
1369
|
+
retries: Optional[int] = None,
|
|
1370
|
+
range_start: Optional[int] = None,
|
|
1371
|
+
range_end: Optional[int] = None,
|
|
1372
|
+
chunk_size: int = 8192,
|
|
1373
|
+
use_public_api: Optional[bool] = True,
|
|
1374
|
+
timeout: httpx._types.TimeoutTypes = 15,
|
|
1375
|
+
) -> AsyncGenerator:
|
|
1376
|
+
"""
|
|
1377
|
+
Performs asynchronous streaming GET or POST request to server with given parameters.
|
|
1378
|
+
Yield chunks of data and hash of the whole content to check integrity of the data stream.
|
|
1379
|
+
|
|
1380
|
+
:param method: Method name for the request.
|
|
1381
|
+
:type method: str
|
|
1382
|
+
:param method_type: Request type ('GET' or 'POST').
|
|
1383
|
+
:type method_type: str
|
|
1384
|
+
:param data: Bytes with data content or dictionary with params.
|
|
1385
|
+
:type data: bytes or dict
|
|
1386
|
+
:param headers: Custom headers to include in the request.
|
|
1387
|
+
:type headers: dict, optional
|
|
1388
|
+
:param retries: The number of retry attempts.
|
|
1389
|
+
:type retries: int, optional
|
|
1390
|
+
:param range_start: Start byte position for streaming.
|
|
1391
|
+
:type range_start: int, optional
|
|
1392
|
+
:param range_end: End byte position for streaming.
|
|
1393
|
+
:type range_end: int, optional
|
|
1394
|
+
:param chunk_size: Size of the chunk to read from the stream.
|
|
1395
|
+
:type chunk_size: int, optional
|
|
1396
|
+
:param use_public_api: Define if public API should be used.
|
|
1397
|
+
:type use_public_api: bool, optional
|
|
1398
|
+
:param timeout: Overall timeout for the request.
|
|
1399
|
+
:type timeout: float, optional
|
|
1400
|
+
:return: Async generator object.
|
|
1401
|
+
:rtype: :class:`AsyncGenerator`
|
|
1402
|
+
"""
|
|
1403
|
+
self._set_async_client()
|
|
1404
|
+
|
|
1405
|
+
if retries is None:
|
|
1406
|
+
retries = self.retry_count
|
|
1407
|
+
|
|
1408
|
+
url = self.api_server_address + "/v3/" + method
|
|
1409
|
+
if not use_public_api:
|
|
1410
|
+
url = os.path.join(self.server_address, method)
|
|
1411
|
+
|
|
1412
|
+
if headers is None:
|
|
1413
|
+
headers = self.headers.copy()
|
|
1414
|
+
else:
|
|
1415
|
+
headers = {**self.headers, **headers}
|
|
1416
|
+
|
|
1417
|
+
logger.trace(f"{method_type} {url}")
|
|
1418
|
+
|
|
1419
|
+
if isinstance(data, (bytes, Generator)):
|
|
1420
|
+
content = data
|
|
1421
|
+
json_body = None
|
|
1422
|
+
params = None
|
|
1423
|
+
elif isinstance(data, Dict):
|
|
1424
|
+
json_body = {**data, **self.additional_fields}
|
|
1425
|
+
content = None
|
|
1426
|
+
params = None
|
|
1427
|
+
else:
|
|
1428
|
+
params = data
|
|
1429
|
+
content = None
|
|
1430
|
+
json_body = None
|
|
1431
|
+
|
|
1432
|
+
if range_start is not None or range_end is not None:
|
|
1433
|
+
headers["Range"] = f"bytes={range_start or ''}-{range_end or ''}"
|
|
1434
|
+
logger.debug(f"Setting Range header: {headers['Range']}")
|
|
1435
|
+
|
|
1436
|
+
for retry_idx in range(retries):
|
|
1437
|
+
total_streamed = 0
|
|
1438
|
+
try:
|
|
1439
|
+
if method_type == "POST":
|
|
1440
|
+
response = self.async_httpx_client.stream(
|
|
1441
|
+
method_type,
|
|
1442
|
+
url,
|
|
1443
|
+
content=content,
|
|
1444
|
+
json=json_body,
|
|
1445
|
+
params=params,
|
|
1446
|
+
headers=headers,
|
|
1447
|
+
timeout=timeout,
|
|
1448
|
+
)
|
|
1449
|
+
elif method_type == "GET":
|
|
1450
|
+
response = self.async_httpx_client.stream(
|
|
1451
|
+
method_type,
|
|
1452
|
+
url,
|
|
1453
|
+
json=json_body or params,
|
|
1454
|
+
headers=headers,
|
|
1455
|
+
timeout=timeout,
|
|
1456
|
+
)
|
|
1457
|
+
else:
|
|
1458
|
+
raise NotImplementedError(
|
|
1459
|
+
f"Unsupported method type: {method_type}. Supported types: 'GET', 'POST'"
|
|
1460
|
+
)
|
|
1461
|
+
|
|
1462
|
+
async with response as resp:
|
|
1463
|
+
expected_size = int(resp.headers.get("content-length", 0))
|
|
1464
|
+
if resp.status_code not in [
|
|
1465
|
+
httpx.codes.OK,
|
|
1466
|
+
httpx.codes.PARTIAL_CONTENT,
|
|
1467
|
+
]:
|
|
1468
|
+
self._check_version()
|
|
1469
|
+
Api._raise_for_status_httpx(resp)
|
|
1470
|
+
|
|
1471
|
+
# received hash of the content to check integrity of the data stream
|
|
1472
|
+
hhash = resp.headers.get("x-content-checksum-sha256", None)
|
|
1473
|
+
async for chunk in resp.aiter_raw(chunk_size):
|
|
1474
|
+
yield chunk, hhash
|
|
1475
|
+
total_streamed += len(chunk)
|
|
1476
|
+
|
|
1477
|
+
if expected_size != 0 and total_streamed != expected_size:
|
|
1478
|
+
raise ValueError(
|
|
1479
|
+
f"Streamed size does not match the expected: {total_streamed} != {expected_size}"
|
|
1480
|
+
)
|
|
1481
|
+
logger.debug(f"Streamed size: {total_streamed}, expected size: {expected_size}")
|
|
1482
|
+
return
|
|
1483
|
+
except httpx.RequestError as e:
|
|
1484
|
+
retry_range_start = total_streamed + (range_start or 0)
|
|
1485
|
+
if total_streamed != 0:
|
|
1486
|
+
retry_range_start += 1
|
|
1487
|
+
headers["Range"] = f"bytes={retry_range_start}-{range_end or ''}"
|
|
1488
|
+
logger.debug(f"Setting Range header {headers['Range']} for retry")
|
|
1489
|
+
process_requests_exception(
|
|
1490
|
+
self.logger,
|
|
1491
|
+
e,
|
|
1492
|
+
method,
|
|
1493
|
+
url,
|
|
1494
|
+
verbose=True,
|
|
1495
|
+
swallow_exc=True,
|
|
1496
|
+
sleep_sec=min(self.retry_sleep_sec * (2**retry_idx), 60),
|
|
1497
|
+
response=locals().get("resp"),
|
|
1498
|
+
retry_info={"retry_idx": retry_idx + 1, "retry_limit": retries},
|
|
1499
|
+
)
|
|
1500
|
+
except Exception as e:
|
|
1501
|
+
process_unhandled_request(self.logger, e)
|
|
1502
|
+
raise httpx.RequestError(
|
|
1503
|
+
message=f"Retry limit exceeded ({url})",
|
|
1504
|
+
request=resp.request if locals().get("resp") else None,
|
|
1505
|
+
)
|
|
1506
|
+
|
|
1507
|
+
def _set_async_client(self):
|
|
1508
|
+
"""
|
|
1509
|
+
Set async httpx client if it is not set yet.
|
|
1510
|
+
Switch on HTTP/2 if the server address uses HTTPS.
|
|
1511
|
+
"""
|
|
1512
|
+
if self.async_httpx_client is None:
|
|
1513
|
+
self._check_https_redirect()
|
|
1514
|
+
if self.server_address.startswith("https://"):
|
|
1515
|
+
self.async_httpx_client = httpx.AsyncClient(http2=True)
|
|
1516
|
+
else:
|
|
1517
|
+
self.async_httpx_client = httpx.AsyncClient()
|
|
1518
|
+
|
|
1519
|
+
def _set_client(self):
|
|
1520
|
+
"""
|
|
1521
|
+
Set httpx client if it is not set yet.
|
|
1522
|
+
Switch on HTTP/2 if the server address uses HTTPS.
|
|
1523
|
+
"""
|
|
1524
|
+
if self.httpx_client is None:
|
|
1525
|
+
self._check_https_redirect()
|
|
1526
|
+
if self.server_address.startswith("https://"):
|
|
1527
|
+
self.httpx_client = httpx.Client(http2=True)
|
|
1528
|
+
else:
|
|
1529
|
+
self.httpx_client = httpx.Client()
|
|
1530
|
+
|
|
1531
|
+
def _get_default_semaphore(self):
|
|
1532
|
+
"""
|
|
1533
|
+
Get default semaphore for async requests.
|
|
1534
|
+
Check if the environment variable SUPERVISELY_ASYNC_SEMAPHORE is set.
|
|
1535
|
+
If it is set, create a semaphore with the given value.
|
|
1536
|
+
Otherwise, create a semaphore with a default value.
|
|
1537
|
+
If server supports HTTPS, create a semaphore with a higher value.
|
|
1538
|
+
"""
|
|
1539
|
+
env_semaphore = os.getenv("SUPERVISELY_ASYNC_SEMAPHORE")
|
|
1540
|
+
if env_semaphore is not None:
|
|
1541
|
+
return asyncio.Semaphore(int(env_semaphore))
|
|
1542
|
+
else:
|
|
1543
|
+
self._check_https_redirect()
|
|
1544
|
+
if self.server_address.startswith("https://"):
|
|
1545
|
+
return asyncio.Semaphore(25)
|
|
1546
|
+
else:
|
|
1547
|
+
return asyncio.Semaphore(5)
|