supervisely 6.73.220__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.

Files changed (27) hide show
  1. supervisely/api/api.py +609 -3
  2. supervisely/api/file_api.py +574 -14
  3. supervisely/api/image_api.py +469 -0
  4. supervisely/api/pointcloud/pointcloud_api.py +390 -1
  5. supervisely/api/video/video_api.py +231 -1
  6. supervisely/api/volume/volume_api.py +223 -2
  7. supervisely/app/development/__init__.py +1 -0
  8. supervisely/app/development/development.py +96 -2
  9. supervisely/app/fastapi/subapp.py +19 -4
  10. supervisely/convert/base_converter.py +53 -4
  11. supervisely/convert/converter.py +6 -5
  12. supervisely/convert/image/image_converter.py +26 -13
  13. supervisely/convert/image/sly/fast_sly_image_converter.py +4 -0
  14. supervisely/convert/image/sly/sly_image_converter.py +9 -4
  15. supervisely/convert/pointcloud_episodes/sly/sly_pointcloud_episodes_converter.py +7 -1
  16. supervisely/convert/video/sly/sly_video_converter.py +9 -1
  17. supervisely/convert/video/video_converter.py +44 -23
  18. supervisely/io/fs.py +125 -0
  19. supervisely/io/fs_cache.py +19 -1
  20. supervisely/io/network_exceptions.py +20 -3
  21. supervisely/task/progress.py +1 -1
  22. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/METADATA +3 -1
  23. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/RECORD +27 -27
  24. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/LICENSE +0 -0
  25. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/WHEEL +0 -0
  26. {supervisely-6.73.220.dist-info → supervisely-6.73.222.dist-info}/entry_points.txt +0 -0
  27. {supervisely-6.73.220.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 Dict, Optional, Union
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.warn(msg)
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)