ap-client 0.4.2.dev0__tar.gz → 0.5.0.dev1__tar.gz
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.
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/PKG-INFO +4 -4
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/akpool_commands.py +3 -1
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/api.py +204 -17
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/cli.py +295 -52
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/config.py +21 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/dataset_commands.py +206 -151
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/exporter.py +1 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/fs_commands.py +45 -28
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/instance_commands.py +74 -53
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/irepo_sdk.py +115 -30
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/split_publish.py +38 -17
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/pyproject.toml +4 -4
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/__init__.py +0 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/managed_ak.py +0 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/profile_commands.py +0 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/tbb.py +0 -0
- {ap_client-0.4.2.dev0 → ap_client-0.5.0.dev1}/ap_client/waiter.py +0 -0
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ap-client
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0.dev1
|
|
4
4
|
Summary: Agent Platform API Client & CLI
|
|
5
5
|
Requires-Python: >=3.10
|
|
6
|
-
Requires-Dist: irepo-artifact==0.
|
|
6
|
+
Requires-Dist: irepo-artifact==0.2.0
|
|
7
7
|
Requires-Dist: pyyaml>=6.0
|
|
8
8
|
Requires-Dist: requests>=2.28.0
|
|
9
9
|
Requires-Dist: rich>=13.0.0
|
|
10
10
|
Requires-Dist: typer>=0.9.0
|
|
11
11
|
Requires-Dist: websockets>=13.0
|
|
12
12
|
Provides-Extra: all
|
|
13
|
-
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.
|
|
13
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.4; extra == 'all'
|
|
14
14
|
Provides-Extra: dataset
|
|
15
|
-
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.
|
|
15
|
+
Requires-Dist: instance-repo[oss]<2.0.0,>=1.1.4; extra == 'dataset'
|
|
16
16
|
Description-Content-Type: text/markdown
|
|
17
17
|
|
|
18
18
|
A lightweight Python SDK and command line interface for Agent Platform. It provides helpers for configuring API access and managing templates, datasets, jobs, and groups.
|
|
@@ -63,7 +63,9 @@ def _fmt(output_format: Optional[str]) -> str:
|
|
|
63
63
|
def _check_time_range(value: str) -> str:
|
|
64
64
|
normalized = str(value or "").strip().lower()
|
|
65
65
|
if not _TIME_RANGE_RE.fullmatch(normalized):
|
|
66
|
-
raise typer.BadParameter(
|
|
66
|
+
raise typer.BadParameter(
|
|
67
|
+
f"--time-range must be a duration like 30s/5m/1h, got {value!r}"
|
|
68
|
+
)
|
|
67
69
|
return normalized
|
|
68
70
|
|
|
69
71
|
|
|
@@ -13,6 +13,7 @@ from typing import Any, Literal, Optional, Union
|
|
|
13
13
|
from urllib.parse import parse_qsl, quote, urlencode, urlsplit, urlunsplit
|
|
14
14
|
|
|
15
15
|
import requests
|
|
16
|
+
from urllib3.util import Timeout as HTTPTimeout
|
|
16
17
|
|
|
17
18
|
from .config import Config, ConfigurationError, get_config, has_cluster_header
|
|
18
19
|
from .managed_ak import require_managed_ak_ack, validate_ak_id, validate_ak_selection
|
|
@@ -126,7 +127,6 @@ EXTRA_AGG_OPS = ("avg", "sum")
|
|
|
126
127
|
_OTHERS_MODEL = {
|
|
127
128
|
"name": "Others",
|
|
128
129
|
"series_name": "",
|
|
129
|
-
"is_default": False,
|
|
130
130
|
"source_id": "",
|
|
131
131
|
"series_id": "",
|
|
132
132
|
}
|
|
@@ -171,7 +171,9 @@ def _merge_meta_tags(
|
|
|
171
171
|
conflicts = [k for k, v in provided.items() if v and str(v).strip() and k in existing_keys]
|
|
172
172
|
if conflicts:
|
|
173
173
|
raise ValueError(
|
|
174
|
-
"tags
|
|
174
|
+
"tags already contains "
|
|
175
|
+
+ ", ".join(conflicts)
|
|
176
|
+
+ "; it conflicts with the meta parameter, keep only one"
|
|
175
177
|
)
|
|
176
178
|
extra = [
|
|
177
179
|
f"{key}:{str(value).strip()}"
|
|
@@ -343,6 +345,11 @@ def _split_paged(payload: Any) -> tuple[list[dict], dict]:
|
|
|
343
345
|
)
|
|
344
346
|
|
|
345
347
|
|
|
348
|
+
# apiserver meta-tags 端点单页上限(handler.metaTagPaging 截断到 200;
|
|
349
|
+
# 缺省仅 20,目录查询必须显式带上并翻页)。
|
|
350
|
+
_META_TAG_PAGE_SIZE = 200
|
|
351
|
+
|
|
352
|
+
|
|
346
353
|
class APIClient:
|
|
347
354
|
"""Agent Platform API client."""
|
|
348
355
|
|
|
@@ -401,6 +408,8 @@ class APIClient:
|
|
|
401
408
|
params: Optional[dict] = None,
|
|
402
409
|
json_body: Any = None,
|
|
403
410
|
timeout: TimeoutType = None,
|
|
411
|
+
deadline: Optional[float] = None,
|
|
412
|
+
_return_response: bool = False,
|
|
404
413
|
) -> Any:
|
|
405
414
|
"""Send an HTTP request with X-Request-ID and optional verbose logging."""
|
|
406
415
|
self._require_cluster_config()
|
|
@@ -412,6 +421,7 @@ class APIClient:
|
|
|
412
421
|
verbose = self.config.verbose
|
|
413
422
|
max_attempts = self._max_attempts_for(method)
|
|
414
423
|
for attempt in range(1, max_attempts + 1):
|
|
424
|
+
attempt_timeout = _deadline_timeout(timeout, deadline)
|
|
415
425
|
request_id = str(uuid.uuid4())
|
|
416
426
|
headers = {"X-Request-ID": request_id}
|
|
417
427
|
if verbose:
|
|
@@ -435,9 +445,11 @@ class APIClient:
|
|
|
435
445
|
params=params,
|
|
436
446
|
json=json_body,
|
|
437
447
|
headers=headers,
|
|
438
|
-
timeout=
|
|
448
|
+
timeout=attempt_timeout,
|
|
439
449
|
)
|
|
440
450
|
except requests.RequestException as exc:
|
|
451
|
+
if deadline is not None:
|
|
452
|
+
_manifest_budget_remaining(deadline)
|
|
441
453
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
442
454
|
if verbose:
|
|
443
455
|
self._log_exception(method, url, request_id, duration_ms, exc)
|
|
@@ -453,10 +465,12 @@ class APIClient:
|
|
|
453
465
|
delay,
|
|
454
466
|
error=f"{type(exc).__name__}: {exc}",
|
|
455
467
|
)
|
|
456
|
-
|
|
468
|
+
_deadline_sleep(delay, deadline)
|
|
457
469
|
continue
|
|
458
470
|
raise APIError(None, str(exc), request_id, None) from exc
|
|
459
471
|
duration_ms = (time.perf_counter() - start) * 1000
|
|
472
|
+
if deadline is not None:
|
|
473
|
+
_manifest_budget_remaining(deadline)
|
|
460
474
|
if verbose:
|
|
461
475
|
self._log_response(method, url, request_id, resp, duration_ms)
|
|
462
476
|
if not resp.ok:
|
|
@@ -472,10 +486,12 @@ class APIClient:
|
|
|
472
486
|
delay,
|
|
473
487
|
status=resp.status_code,
|
|
474
488
|
)
|
|
475
|
-
|
|
489
|
+
_deadline_sleep(delay, deadline)
|
|
476
490
|
continue
|
|
477
491
|
detail = _response_error_detail(resp)
|
|
478
492
|
raise APIError(resp.status_code, detail, request_id, resp)
|
|
493
|
+
if _return_response:
|
|
494
|
+
return resp, request_id
|
|
479
495
|
if not resp.content:
|
|
480
496
|
return None
|
|
481
497
|
try:
|
|
@@ -977,21 +993,103 @@ class APIClient:
|
|
|
977
993
|
return item
|
|
978
994
|
raise APIError(404, f"meta model {name!r} not found", "client-filter", None)
|
|
979
995
|
|
|
996
|
+
def _get_meta_tag_catalog(
|
|
997
|
+
self, path: str, params: Optional[dict] = None
|
|
998
|
+
) -> tuple[list[dict], dict]:
|
|
999
|
+
"""按 benchmarks 同口径自动翻页取全量目录记录,返回 ``(records, pagination)``。
|
|
1000
|
+
|
|
1001
|
+
apiserver ``meta-tags`` 端点分页参数为 ``page`` / ``page_size``(缺省
|
|
1002
|
+
20、上限 200,见 handler.metaTagPaging);单页取用会把值目录静默截断
|
|
1003
|
+
(``meta-job-type`` 漏掉 ``Eval-Eval`` 时 ``default`` 会被算成空串),
|
|
1004
|
+
故显式传上限并翻页。
|
|
1005
|
+
"""
|
|
1006
|
+
records: list[dict] = []
|
|
1007
|
+
pagination: dict = {}
|
|
1008
|
+
page = 1
|
|
1009
|
+
# 与 list_benchmarks 同口径的 100 页上限,防分页异常导致死循环
|
|
1010
|
+
max_pages = 100
|
|
1011
|
+
while page <= max_pages:
|
|
1012
|
+
query = dict(params or {})
|
|
1013
|
+
query["page"] = page
|
|
1014
|
+
query["page_size"] = _META_TAG_PAGE_SIZE
|
|
1015
|
+
page_records, pagination = _split_paged(self._get_central(path, params=query))
|
|
1016
|
+
records.extend(page_records)
|
|
1017
|
+
if not page_records or not pagination:
|
|
1018
|
+
break
|
|
1019
|
+
try:
|
|
1020
|
+
total = int(pagination.get("total"))
|
|
1021
|
+
except (TypeError, ValueError):
|
|
1022
|
+
total = None
|
|
1023
|
+
if total is not None:
|
|
1024
|
+
if len(records) >= total:
|
|
1025
|
+
break
|
|
1026
|
+
elif len(page_records) < _META_TAG_PAGE_SIZE:
|
|
1027
|
+
break
|
|
1028
|
+
page += 1
|
|
1029
|
+
return records, pagination
|
|
1030
|
+
|
|
1031
|
+
def list_meta_tags(self) -> dict:
|
|
1032
|
+
"""meta 标签键目录(apiserver GET /apis/v1/meta-tags/keys,返回 {items:[...]})。
|
|
1033
|
+
|
|
1034
|
+
apiserver 用 PagedSuccess envelope({code,data,pagination})返回,
|
|
1035
|
+
按基准 benchmarks 客户端同口径经 _split_paged 拆包并自动翻页;
|
|
1036
|
+
裸列表等旧形态兜底为空分页。
|
|
1037
|
+
"""
|
|
1038
|
+
records, pagination = self._get_meta_tag_catalog("/meta-tags/keys")
|
|
1039
|
+
result = {"items": records}
|
|
1040
|
+
if pagination:
|
|
1041
|
+
result["pagination"] = pagination
|
|
1042
|
+
return result
|
|
1043
|
+
|
|
1044
|
+
def get_meta_tags(self, key: str) -> dict:
|
|
1045
|
+
"""单个 meta 键的值目录(GET /apis/v1/meta-tags?key=,返回 {items:[...]})。
|
|
1046
|
+
|
|
1047
|
+
与 ``list_meta_tags`` 同样自动翻页,避免值目录被单页默认大小截断。
|
|
1048
|
+
"""
|
|
1049
|
+
records, pagination = self._get_meta_tag_catalog("/meta-tags", {"key": key})
|
|
1050
|
+
result = {"items": records}
|
|
1051
|
+
if pagination:
|
|
1052
|
+
result["pagination"] = pagination
|
|
1053
|
+
return result
|
|
1054
|
+
|
|
980
1055
|
def list_meta_job_types(self) -> dict:
|
|
981
|
-
"""
|
|
1056
|
+
"""任务类型词表(目录驱动:GET /apis/v1/meta-tags?key=meta-job-type)。
|
|
982
1057
|
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1058
|
+
仅取目录中 ``is_enabled`` 的值(缺失按启用解析)+ 末尾 ``Others``
|
|
1059
|
+
兜底行;``default`` 为 ``Eval-Eval``(在目录中时,否则空串)。
|
|
1060
|
+
目录请求失败或为空 → 回退本地常量(Eval-Eval + Others,fail-open),
|
|
1061
|
+
并在 stderr 打一行告警,词表降级可见但不阻塞命令。
|
|
986
1062
|
"""
|
|
987
|
-
|
|
1063
|
+
error: Exception | None = None
|
|
1064
|
+
try:
|
|
1065
|
+
items = self.get_meta_tags("meta-job-type").get("items") or []
|
|
1066
|
+
except Exception as exc: # fail-open:目录不可用不阻塞词表命令
|
|
1067
|
+
error = exc
|
|
1068
|
+
items = []
|
|
1069
|
+
if items:
|
|
1070
|
+
options = []
|
|
1071
|
+
for item in items:
|
|
1072
|
+
value = str(item.get("value") or "").strip()
|
|
1073
|
+
if value and item.get("is_enabled", True):
|
|
1074
|
+
options.append({"value": value})
|
|
1075
|
+
if options:
|
|
1076
|
+
default = "Eval-Eval" if any(o["value"] == "Eval-Eval" for o in options) else ""
|
|
1077
|
+
return {
|
|
1078
|
+
"meta_job_type": {
|
|
1079
|
+
"default": default,
|
|
1080
|
+
"options": [*options, {"value": "Others"}],
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
_write_stderr(
|
|
1084
|
+
[
|
|
1085
|
+
"warning: meta-job-type catalog unavailable or empty, "
|
|
1086
|
+
f"falling back to local constants (reason={error or 'empty catalog'})"
|
|
1087
|
+
]
|
|
1088
|
+
)
|
|
988
1089
|
return {
|
|
989
1090
|
"meta_job_type": {
|
|
990
|
-
"default":
|
|
991
|
-
"options": [
|
|
992
|
-
{"value": default, "is_default": True},
|
|
993
|
-
{"value": "Others", "is_default": False},
|
|
994
|
-
],
|
|
1091
|
+
"default": "Eval-Eval",
|
|
1092
|
+
"options": [{"value": "Eval-Eval"}, {"value": "Others"}],
|
|
995
1093
|
}
|
|
996
1094
|
}
|
|
997
1095
|
|
|
@@ -1185,6 +1283,7 @@ class APIClient:
|
|
|
1185
1283
|
benchmark: Optional[str] = None,
|
|
1186
1284
|
experiment: Optional[str] = None,
|
|
1187
1285
|
ak_id: Optional[str] = None,
|
|
1286
|
+
ak_mode: str = "auto",
|
|
1188
1287
|
instance_range: Optional[str] = None,
|
|
1189
1288
|
profile: Optional[str] = None,
|
|
1190
1289
|
) -> dict:
|
|
@@ -1236,6 +1335,7 @@ class APIClient:
|
|
|
1236
1335
|
benchmark=benchmark,
|
|
1237
1336
|
experiment=experiment,
|
|
1238
1337
|
ak_id=ak_id,
|
|
1338
|
+
ak_mode=ak_mode,
|
|
1239
1339
|
)
|
|
1240
1340
|
|
|
1241
1341
|
result = self._post("/jobs", body, timeout=timeout)
|
|
@@ -1297,6 +1397,7 @@ class APIClient:
|
|
|
1297
1397
|
benchmark: Optional[str] = None,
|
|
1298
1398
|
experiment: Optional[str] = None,
|
|
1299
1399
|
ak_id: Optional[str] = None,
|
|
1400
|
+
ak_mode: str = "auto",
|
|
1300
1401
|
instance_range: Optional[str] = None,
|
|
1301
1402
|
profile: Optional[str] = None,
|
|
1302
1403
|
) -> dict:
|
|
@@ -1312,11 +1413,15 @@ class APIClient:
|
|
|
1312
1413
|
if not template and not profile_id and profile is None:
|
|
1313
1414
|
raise ValueError("Either template, profile or profile_id is required")
|
|
1314
1415
|
validate_ak_selection(ak_id, params, params_list, overrides)
|
|
1416
|
+
if ak_mode not in {"auto", "manual", "synced"}:
|
|
1417
|
+
raise ValueError("ak_mode must be auto, manual or synced")
|
|
1315
1418
|
body: dict = {}
|
|
1316
1419
|
if template is not None:
|
|
1317
1420
|
body["template"] = template
|
|
1318
1421
|
if ak_id is not None:
|
|
1319
1422
|
body["ak_id"] = ak_id
|
|
1423
|
+
if ak_mode != "auto":
|
|
1424
|
+
body["ak_mode"] = ak_mode
|
|
1320
1425
|
|
|
1321
1426
|
if params is not None:
|
|
1322
1427
|
body["params"] = params
|
|
@@ -2067,9 +2172,14 @@ class APIClient:
|
|
|
2067
2172
|
return self._delete(f"/concurrency-policies/{quote(policy_id)}")
|
|
2068
2173
|
|
|
2069
2174
|
# ==================== Logs and artifacts ====================
|
|
2070
|
-
def
|
|
2175
|
+
def preview_group_cancel(self, group_id: str) -> dict:
|
|
2176
|
+
"""Preview the current Group cancel scope and obtain confirmation if required."""
|
|
2177
|
+
return self._post(f"/groups/{quote(group_id)}/cancel-preview", {})
|
|
2178
|
+
|
|
2179
|
+
def cancel_group(self, group_id: str, confirmation_token: Optional[str] = None) -> dict:
|
|
2071
2180
|
"""Cancel all unfinished jobs in a group."""
|
|
2072
|
-
|
|
2181
|
+
body = {"confirmation_token": confirmation_token} if confirmation_token else {}
|
|
2182
|
+
return self._post(f"/groups/{quote(group_id)}/cancel", body)
|
|
2073
2183
|
|
|
2074
2184
|
def get_job_logs(
|
|
2075
2185
|
self,
|
|
@@ -2362,6 +2472,52 @@ class APIClient:
|
|
|
2362
2472
|
params=params,
|
|
2363
2473
|
)
|
|
2364
2474
|
|
|
2475
|
+
def wait_for_job_artifacts_manifest(self, job_id: str) -> dict:
|
|
2476
|
+
"""Poll the new API within one per-Job budget; HTTP failures keep normal retries."""
|
|
2477
|
+
budget = self.config.artifact_manifest_wait_timeout
|
|
2478
|
+
if not math.isfinite(budget) or budget <= 0:
|
|
2479
|
+
raise ConfigurationError(
|
|
2480
|
+
"AP_ARTIFACT_MANIFEST_WAIT_TIMEOUT must be a finite positive number"
|
|
2481
|
+
)
|
|
2482
|
+
deadline = time.monotonic() + budget
|
|
2483
|
+
while True:
|
|
2484
|
+
response, request_id = self._request(
|
|
2485
|
+
"GET",
|
|
2486
|
+
f"/jobs/{quote(job_id, safe='')}/artifacts/manifest",
|
|
2487
|
+
deadline=deadline,
|
|
2488
|
+
_return_response=True,
|
|
2489
|
+
)
|
|
2490
|
+
try:
|
|
2491
|
+
payload = response.json()
|
|
2492
|
+
except ValueError as exc:
|
|
2493
|
+
raise APIError(
|
|
2494
|
+
response.status_code, "Invalid artifact manifest response", request_id, response
|
|
2495
|
+
) from exc
|
|
2496
|
+
if not isinstance(payload, dict) or payload.get("job_id") != job_id:
|
|
2497
|
+
raise APIError(
|
|
2498
|
+
response.status_code, "Invalid artifact manifest response", request_id, response
|
|
2499
|
+
)
|
|
2500
|
+
if response.status_code == 200:
|
|
2501
|
+
if "manifest" not in payload or (
|
|
2502
|
+
payload["manifest"] is not None and not isinstance(payload["manifest"], list)
|
|
2503
|
+
):
|
|
2504
|
+
raise APIError(200, "Invalid artifact manifest response", request_id, response)
|
|
2505
|
+
return payload
|
|
2506
|
+
if response.status_code != 202 or payload.get("status") != "preparing":
|
|
2507
|
+
raise APIError(
|
|
2508
|
+
response.status_code,
|
|
2509
|
+
"Invalid artifact preparation response",
|
|
2510
|
+
request_id,
|
|
2511
|
+
response,
|
|
2512
|
+
)
|
|
2513
|
+
try:
|
|
2514
|
+
delay = float(response.headers.get("Retry-After", "2"))
|
|
2515
|
+
except ValueError:
|
|
2516
|
+
delay = 2.0
|
|
2517
|
+
if not math.isfinite(delay) or delay <= 0:
|
|
2518
|
+
delay = 2.0
|
|
2519
|
+
_deadline_sleep(delay, deadline)
|
|
2520
|
+
|
|
2365
2521
|
def get_artifact_file_download_urls(self, job_id: str, paths: list[str]) -> dict:
|
|
2366
2522
|
"""Get download URLs for multiple artifact files."""
|
|
2367
2523
|
endpoint = f"/jobs/{quote(job_id, safe='')}/artifacts/files/download"
|
|
@@ -3275,3 +3431,34 @@ def _summarize_json_value(data: Any, limit: int) -> str:
|
|
|
3275
3431
|
summary = {"_count": len(data), "_sample": sample}
|
|
3276
3432
|
return _truncate_text(_safe_json(summary), limit)
|
|
3277
3433
|
return _truncate_text(_safe_json(data), limit)
|
|
3434
|
+
|
|
3435
|
+
|
|
3436
|
+
def _manifest_budget_remaining(deadline: float) -> float:
|
|
3437
|
+
remaining = deadline - time.monotonic()
|
|
3438
|
+
if remaining <= 0:
|
|
3439
|
+
raise APIError(None, "Artifact manifest wait timed out", None, None)
|
|
3440
|
+
return remaining
|
|
3441
|
+
|
|
3442
|
+
|
|
3443
|
+
def _deadline_timeout(
|
|
3444
|
+
timeout: TimeoutType, deadline: Optional[float]
|
|
3445
|
+
) -> Union[TimeoutType, HTTPTimeout]:
|
|
3446
|
+
if deadline is None:
|
|
3447
|
+
return timeout
|
|
3448
|
+
remaining = _manifest_budget_remaining(deadline)
|
|
3449
|
+
|
|
3450
|
+
def bounded(value):
|
|
3451
|
+
return min(value, remaining) if value is not None else remaining
|
|
3452
|
+
|
|
3453
|
+
if isinstance(timeout, tuple):
|
|
3454
|
+
connect, read = (bounded(value) for value in timeout)
|
|
3455
|
+
else:
|
|
3456
|
+
connect = read = bounded(timeout)
|
|
3457
|
+
# The connect phase must consume the same remaining budget as the read.
|
|
3458
|
+
return HTTPTimeout(total=remaining, connect=connect, read=read)
|
|
3459
|
+
|
|
3460
|
+
|
|
3461
|
+
def _deadline_sleep(delay: float, deadline: Optional[float]) -> None:
|
|
3462
|
+
if deadline is not None:
|
|
3463
|
+
delay = min(delay, _manifest_budget_remaining(deadline))
|
|
3464
|
+
time.sleep(delay)
|