envloop-cli 1.13.0__tar.gz → 1.16.0__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.
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/PKG-INFO +26 -3
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/README.pypi.md +25 -2
- envloop_cli-1.16.0/envloop/__init__.py +6 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_cli.py +4 -2
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_login.py +5 -3
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli.py +16 -7
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli_errors.py +1 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/client.py +2 -0
- envloop_cli-1.16.0/envloop/decision.py +19 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/http.py +33 -5
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/inference.py +1 -1
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/pyproject.toml +1 -1
- envloop_cli-1.13.0/envloop/__init__.py +0 -5
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/.gitignore +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/agent_runs.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/cli_output.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/config.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/job_logs.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/job_spec_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/lifecycle_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/navigation_resources.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/profile_secrets.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/profile_updates.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/resource_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/resources.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/rsi_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/sandbox.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/terminal.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_log_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_progress.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/train_runner_cli.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/training/__init__.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-1.13.0 → envloop_cli-1.16.0}/envloop/utils/logging.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: envloop-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
|
|
5
5
|
Project-URL: Homepage, https://envloop.ai
|
|
6
6
|
Project-URL: Documentation, https://docs.envloop.ai
|
|
@@ -192,8 +192,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
192
192
|
{
|
|
193
193
|
"request_id": "gpu-run-001",
|
|
194
194
|
"trials": [
|
|
195
|
-
{"name": "first", "spec": {"runner": "envhub/bash
|
|
196
|
-
{"name": "second", "spec": {"runner": "envhub/bash
|
|
195
|
+
{"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
|
|
196
|
+
{"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
|
|
197
197
|
]
|
|
198
198
|
}
|
|
199
199
|
```
|
|
@@ -217,3 +217,26 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
|
|
|
217
217
|
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
218
218
|
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
219
219
|
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
220
|
+
|
|
221
|
+
## Job-bound decision plugins
|
|
222
|
+
|
|
223
|
+
When the platform and published Runner support Job Profiles, include the authorized
|
|
224
|
+
`profile` snapshot in the Train or Inference request's `spec`. The platform freezes
|
|
225
|
+
its inputs, recipe and approved plugin pins. This does not select an executor or provider.
|
|
226
|
+
|
|
227
|
+
For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```python
|
|
234
|
+
with Client.remote() as client:
|
|
235
|
+
result = client.decision.evaluate(
|
|
236
|
+
"JOB_ID", model="WORKSPACE/MODEL", payload=payload
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Use the exact model returned by the Job. The SDK forwards the payload to the
|
|
241
|
+
Job's `/Inference/decision` API; the approved plugin validates its business fields.
|
|
242
|
+
Availability requires matching platform support and approved content in your project.
|
|
@@ -178,8 +178,8 @@ Choose a published Runner readable in your project. Save this request as `gpu-ru
|
|
|
178
178
|
{
|
|
179
179
|
"request_id": "gpu-run-001",
|
|
180
180
|
"trials": [
|
|
181
|
-
{"name": "first", "spec": {"runner": "envhub/bash
|
|
182
|
-
{"name": "second", "spec": {"runner": "envhub/bash
|
|
181
|
+
{"name": "first", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}},
|
|
182
|
+
{"name": "second", "spec": {"runner": "envhub/bash@FULL_COMMIT", "resources": {"gpu_type": "H100", "gpus": 4}, "timeout_sec": 600}}
|
|
183
183
|
]
|
|
184
184
|
}
|
|
185
185
|
```
|
|
@@ -203,3 +203,26 @@ Use `el --no-color job list` or append `--no-color` to a command to disable high
|
|
|
203
203
|
`NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
|
|
204
204
|
List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
|
|
205
205
|
`el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
|
|
206
|
+
|
|
207
|
+
## Job-bound decision plugins
|
|
208
|
+
|
|
209
|
+
When the platform and published Runner support Job Profiles, include the authorized
|
|
210
|
+
`profile` snapshot in the Train or Inference request's `spec`. The platform freezes
|
|
211
|
+
its inputs, recipe and approved plugin pins. This does not select an executor or provider.
|
|
212
|
+
|
|
213
|
+
For an Inference Job with a bound decision plugin, save the plugin payload as JSON:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
el job decision JOB_ID --model WORKSPACE/MODEL --spec payload.json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
with Client.remote() as client:
|
|
221
|
+
result = client.decision.evaluate(
|
|
222
|
+
"JOB_ID", model="WORKSPACE/MODEL", payload=payload
|
|
223
|
+
)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use the exact model returned by the Job. The SDK forwards the payload to the
|
|
227
|
+
Job's `/Inference/decision` API; the approved plugin validates its business fields.
|
|
228
|
+
Availability requires matching platform support and approved content in your project.
|
|
@@ -130,7 +130,8 @@ def _verify_key(url, key, workspace_id=None, project_id=None):
|
|
|
130
130
|
response = HttpService(url, timeout=5, api_key=key, project_id=project_id, config=False,
|
|
131
131
|
environment=False).request("GET", "/v1/me")
|
|
132
132
|
except RemoteError as error:
|
|
133
|
-
code =
|
|
133
|
+
code = "EDGE_REJECTED" if error.code == "EDGE_REJECTED" else {
|
|
134
|
+
401: "AUTHENTICATION_REJECTED", 403: "AUTHORIZATION_DENIED"}.get(error.status_code)
|
|
134
135
|
if code is None:
|
|
135
136
|
code = error.code if error.status_code is None and error.code in {
|
|
136
137
|
"NETWORK_ERROR", "INVALID_RESPONSE", "RESPONSE_TOO_LARGE",
|
|
@@ -279,7 +280,8 @@ def run_auth(args, parser):
|
|
|
279
280
|
except (ConfigError, RemoteError) as error:
|
|
280
281
|
from .cli_errors import format_error, safe_error_code
|
|
281
282
|
|
|
282
|
-
report["check"] =
|
|
283
|
+
report["check"] = "edge_rejected" if getattr(error, "code", None) == "EDGE_REJECTED" else {
|
|
284
|
+
401: "authentication_rejected", 403: "authorization_denied"}.get(
|
|
283
285
|
getattr(error, "status_code", None), safe_error_code(error))
|
|
284
286
|
_print(report, args.json)
|
|
285
287
|
print(format_error(error), file=sys.stderr)
|
|
@@ -7,18 +7,18 @@ import webbrowser
|
|
|
7
7
|
from http.client import InvalidURL
|
|
8
8
|
from urllib.error import HTTPError, URLError
|
|
9
9
|
from urllib.parse import parse_qs, urlsplit
|
|
10
|
-
from urllib.request import
|
|
10
|
+
from urllib.request import Request
|
|
11
11
|
|
|
12
12
|
from .auth_workspace import _identity
|
|
13
13
|
from .config import ConfigError, normalize_url
|
|
14
|
-
from .http import
|
|
14
|
+
from .http import build_client_opener, error_fields
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def _request(url, action, body, timeout):
|
|
18
18
|
request = Request(url + '/v1/auth/cli/' + action, data=json.dumps(body).encode(),
|
|
19
19
|
headers={'Content-Type': 'application/json'}, method='POST')
|
|
20
20
|
try:
|
|
21
|
-
with
|
|
21
|
+
with build_client_opener().open(request, timeout=timeout) as response:
|
|
22
22
|
raw = response.read(8193)
|
|
23
23
|
if len(raw) > 8192:
|
|
24
24
|
raise ConfigError('AUTH_LOGIN_INVALID_RESPONSE')
|
|
@@ -27,6 +27,8 @@ def _request(url, action, body, timeout):
|
|
|
27
27
|
raise ConfigError('AUTH_LOGIN_INVALID_RESPONSE')
|
|
28
28
|
return result
|
|
29
29
|
except HTTPError as error:
|
|
30
|
+
if error.code == 403 and error_fields(error)['code'] == 'EDGE_REJECTED':
|
|
31
|
+
raise ConfigError('EDGE_REJECTED') from None
|
|
30
32
|
code = {403: 'AUTHORIZATION_DENIED', 409: 'AUTH_LOGIN_UNAVAILABLE', 410: 'AUTH_LOGIN_EXPIRED',
|
|
31
33
|
429: 'AUTH_LOGIN_BUSY', 503: 'AUTH_LOGIN_UNAVAILABLE'}.get(error.code, 'AUTH_LOGIN_HTTP_ERROR')
|
|
32
34
|
raise ConfigError(code) from None
|
|
@@ -6,16 +6,10 @@ import math
|
|
|
6
6
|
import os
|
|
7
7
|
import shlex
|
|
8
8
|
import sys
|
|
9
|
-
from importlib.metadata import PackageNotFoundError, version
|
|
10
9
|
from pathlib import Path
|
|
11
10
|
|
|
12
11
|
from .client import Client
|
|
13
|
-
|
|
14
|
-
try:
|
|
15
|
-
CLI_VERSION = version("envloop-cli")
|
|
16
|
-
except PackageNotFoundError:
|
|
17
|
-
CLI_VERSION = "unknown"
|
|
18
|
-
|
|
12
|
+
from .http import CLI_VERSION
|
|
19
13
|
|
|
20
14
|
SECTIONS = {
|
|
21
15
|
'OVERVIEW': ('dashboard',),
|
|
@@ -210,6 +204,10 @@ def parser():
|
|
|
210
204
|
if action == 'event':
|
|
211
205
|
command.add_argument('--json', action='store_true')
|
|
212
206
|
command.set_defaults(_list_view=True)
|
|
207
|
+
decision = jobs.add_parser("decision", help="Invoke the frozen decision plugin of an Inference Job")
|
|
208
|
+
decision.add_argument("job_id")
|
|
209
|
+
decision.add_argument("--model", required=True)
|
|
210
|
+
decision.add_argument("--spec", type=Path, required=True, metavar="PAYLOAD.json")
|
|
213
211
|
from .resource_cli import add_job_resources
|
|
214
212
|
add_job_resources(jobs)
|
|
215
213
|
listing = jobs.add_parser("list")
|
|
@@ -434,6 +432,15 @@ def _main(argv=None):
|
|
|
434
432
|
|
|
435
433
|
print(format_error(exc), file=sys.stderr)
|
|
436
434
|
return 1
|
|
435
|
+
if args.group == "job" and args.action == "decision":
|
|
436
|
+
from .decision import DecisionClient
|
|
437
|
+
try:
|
|
438
|
+
if args.spec.stat().st_size > 1024 * 1024:
|
|
439
|
+
raise ValueError("Decision payload exceeds 1 MiB")
|
|
440
|
+
args.payload = json.loads(args.spec.read_text(encoding="utf-8"))
|
|
441
|
+
DecisionClient.validate(args.job_id, args.model, args.payload)
|
|
442
|
+
except (OSError, ValueError, UnicodeError):
|
|
443
|
+
cli.error("decision requires a readable JSON object of at most 1 MiB, without model")
|
|
437
444
|
if args.group == "job" and args.action == "submit":
|
|
438
445
|
from .train_runner_cli import SHORTCUT_FIELDS, runner_request
|
|
439
446
|
|
|
@@ -586,6 +593,8 @@ def _main(argv=None):
|
|
|
586
593
|
if args.wait:
|
|
587
594
|
wait_options = {"timeout": args.timeout_sec} if args.type == "train" and args.spec is not None and args.timeout_sec is not None else {}
|
|
588
595
|
result = client.jobs.wait(result["job_id"], **wait_options)
|
|
596
|
+
elif args.action == "decision":
|
|
597
|
+
result = client.decision.evaluate(args.job_id, model=args.model, payload=args.payload)
|
|
589
598
|
elif args.action == 'event':
|
|
590
599
|
result = client.jobs.events(args.job_id)
|
|
591
600
|
elif args.action in {'serving-profile', 'adapter'}:
|
|
@@ -29,6 +29,7 @@ _register("HTTP_ERROR", "The API request failed.")
|
|
|
29
29
|
_register("NOT_FOUND RESOURCE_NOT_FOUND JOB_NOT_FOUND RUNNER_NOT_FOUND DATASET_REVISION_NOT_FOUND PROFILE_NOT_FOUND WORKSPACE_NOT_FOUND CHECKPOINT_NOT_FOUND AGENT_RUN_NOT_FOUND ARTIFACT_NOT_FOUND", "The requested API resource or route was not found.")
|
|
30
30
|
_register("UNAUTHORIZED UNAUTHENTICATED INVALID_AUTH AUTHENTICATION_REQUIRED AUTHENTICATION_REJECTED API_KEY_REQUIRED", "API key authentication is required or was rejected.")
|
|
31
31
|
_register("FORBIDDEN ACCESS_DENIED AUTHORIZATION_DENIED PERMISSION_DENIED WORKSPACE_ACCESS_DENIED PROJECT_ACCESS_DENIED", "The current credentials do not have permission for this operation.")
|
|
32
|
+
_register("EDGE_REJECTED", "The network edge (Cloudflare) rejected the request before it reached the EnvLoop API; this is not a permission decision. Upgrade envloop-cli, check any proxy or custom User-Agent, or contact support with the HTTP status.")
|
|
32
33
|
_register("HUMAN_SESSION_REQUIRED WORKSPACE_LIST_REQUIRES_SESSION PROJECT_LIST_REQUIRES_SESSION", "This operation requires a browser session authorized by the server.")
|
|
33
34
|
_register("CONFLICT VERSION_CONFLICT RESOURCE_VERSION_CONFLICT PRECONDITION_FAILED IDEMPOTENCY_CONFLICT", "The request conflicts with the current resource version or an earlier request.")
|
|
34
35
|
_register("BAD_REQUEST VALIDATION_ERROR VALIDATION_FAILED INVALID_REQUEST INVALID_ARGUMENT INVALID_CURSOR INVALID_PROFILE INVALID_PROFILE_SECRETS", "The request or input is invalid.")
|
|
@@ -621,6 +621,7 @@ class Client:
|
|
|
621
621
|
"""A client for a caller-owned service, or use local() for an owned server."""
|
|
622
622
|
|
|
623
623
|
def __init__(self, server: RpcService) -> None:
|
|
624
|
+
from .decision import DecisionClient
|
|
624
625
|
from .navigation_resources import (
|
|
625
626
|
Billing,
|
|
626
627
|
Checkpoints,
|
|
@@ -632,6 +633,7 @@ class Client:
|
|
|
632
633
|
from .resources import Adapters, Endpoints, Operations
|
|
633
634
|
|
|
634
635
|
self._transport = server
|
|
636
|
+
self.decision = DecisionClient(server)
|
|
635
637
|
self.tasks = Tasks(server)
|
|
636
638
|
self.trials = self.tasks
|
|
637
639
|
self.profiles = Profiles(server)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Thin Job-bound decision transport. Business logic stays in approved plugins."""
|
|
2
|
+
from urllib.parse import quote
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class DecisionClient:
|
|
6
|
+
def __init__(self, transport):
|
|
7
|
+
self.transport = transport
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def validate(job_id, model, payload):
|
|
11
|
+
if (not isinstance(job_id, str) or not job_id.strip()
|
|
12
|
+
or not isinstance(model, str) or not model.strip()
|
|
13
|
+
or not isinstance(payload, dict) or 'model' in payload):
|
|
14
|
+
raise ValueError('decision requires job_id, model and a payload object without model')
|
|
15
|
+
|
|
16
|
+
def evaluate(self, job_id, *, model, payload):
|
|
17
|
+
self.validate(job_id, model, payload)
|
|
18
|
+
return self.transport.request('POST', f'/v1/job/{quote(job_id, safe="")}/Inference/decision',
|
|
19
|
+
{'model': model, **payload})
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
import base64
|
|
4
4
|
import json
|
|
5
5
|
import math
|
|
6
|
+
import platform
|
|
6
7
|
import re
|
|
7
8
|
from datetime import datetime, timezone
|
|
8
9
|
from email.utils import parsedate_to_datetime
|
|
9
10
|
from http.client import HTTPException, InvalidURL
|
|
11
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
12
|
from urllib.error import HTTPError, URLError
|
|
11
13
|
from urllib.parse import quote, urlencode, urljoin, urlsplit
|
|
12
14
|
from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener
|
|
@@ -66,6 +68,23 @@ class NoRedirect(HTTPRedirectHandler):
|
|
|
66
68
|
return None
|
|
67
69
|
|
|
68
70
|
|
|
71
|
+
try:
|
|
72
|
+
CLI_VERSION = version("envloop-cli")
|
|
73
|
+
except PackageNotFoundError:
|
|
74
|
+
CLI_VERSION = "unknown"
|
|
75
|
+
|
|
76
|
+
# Cloudflare bot rules answer urllib's default "Python-urllib/3.x" with error 1010 (staging workers.dev).
|
|
77
|
+
USER_AGENT = (f"envloop-cli/{CLI_VERSION} (python/{platform.python_version()}; "
|
|
78
|
+
f"{'-'.join(filter(None, (platform.system().lower(), platform.machine().lower()))) or 'unknown'})")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_client_opener():
|
|
82
|
+
"""Proxy-aware opener that never follows redirects and names the client in every request."""
|
|
83
|
+
opener = build_opener(ProxyHandler(), NoRedirect())
|
|
84
|
+
opener.addheaders = [("User-Agent", USER_AGENT)]
|
|
85
|
+
return opener
|
|
86
|
+
|
|
87
|
+
|
|
69
88
|
def _text(value, limit, secrets=()):
|
|
70
89
|
"""One printable line: no control characters, no submitted secret or URL query, bounded length."""
|
|
71
90
|
if not isinstance(value, str):
|
|
@@ -101,10 +120,12 @@ def error_fields(error, secrets=()):
|
|
|
101
120
|
Reads top-level `code/message/hint/request_id/fields/details` or the same
|
|
102
121
|
keys nested in `error: {...}`; request_id falls back to X-Request-ID.
|
|
103
122
|
"""
|
|
123
|
+
raw, parsed = b"", True
|
|
104
124
|
try:
|
|
105
|
-
|
|
125
|
+
raw = error.read(8192)
|
|
126
|
+
data = json.loads(raw)
|
|
106
127
|
except (ValueError, UnicodeError, OSError, HTTPException):
|
|
107
|
-
data = None
|
|
128
|
+
data, parsed = None, False
|
|
108
129
|
finally:
|
|
109
130
|
error.close()
|
|
110
131
|
data = data if isinstance(data, dict) else {}
|
|
@@ -115,6 +136,13 @@ def error_fields(error, secrets=()):
|
|
|
115
136
|
return nested[key] if key in nested else data.get(key)
|
|
116
137
|
|
|
117
138
|
code = pick("code")
|
|
139
|
+
# The platform answers in JSON. A 403 whose body is non-JSON text (Cloudflare's "error code: 1010") or carries
|
|
140
|
+
# Cloudflare's numeric error_code was produced by the edge before the API saw the credential: not an RBAC
|
|
141
|
+
# decision. A JSON 403 without a code, even an empty one, stays a platform authorization answer.
|
|
142
|
+
edge_text = not parsed and bool(raw.strip())
|
|
143
|
+
edge_code = isinstance(data.get("error_code"), int) and not isinstance(data.get("error_code"), bool)
|
|
144
|
+
if error.code == 403 and not isinstance(code, str) and (edge_text or edge_code):
|
|
145
|
+
code = "EDGE_REJECTED"
|
|
118
146
|
if (not isinstance(code, str) or not code or len(code) > 128 or not code.isascii()
|
|
119
147
|
or not all("A" <= character <= "Z" or character.isdigit() or character == "_" for character in code)):
|
|
120
148
|
code = "HTTP_ERROR"
|
|
@@ -169,7 +197,7 @@ class HttpService:
|
|
|
169
197
|
self.base = connection.api_url
|
|
170
198
|
self.secret = connection.api_key
|
|
171
199
|
self.timeout = timeout
|
|
172
|
-
self.opener =
|
|
200
|
+
self.opener = build_client_opener()
|
|
173
201
|
|
|
174
202
|
def remote_error(self, error, body=None, **options):
|
|
175
203
|
submitted = body.get("secrets") if isinstance(body, dict) else None
|
|
@@ -210,7 +238,7 @@ class HttpService:
|
|
|
210
238
|
or parts.username is not None or parts.password is not None or parts.fragment):
|
|
211
239
|
raise RemoteError('UNSAFE_ARTIFACT_REDIRECT')
|
|
212
240
|
try:
|
|
213
|
-
return Download(
|
|
241
|
+
return Download(build_client_opener().open(url, timeout=self.timeout))
|
|
214
242
|
except HTTPError as error:
|
|
215
243
|
error.close()
|
|
216
244
|
raise RemoteError('ARTIFACT_DOWNLOAD_FAILED', error.code) from None
|
|
@@ -309,7 +337,7 @@ class HttpService:
|
|
|
309
337
|
# A presigned object URL carries its own authority: use a separate opener
|
|
310
338
|
# with no Authorization / workspace / project headers and no further hops.
|
|
311
339
|
try:
|
|
312
|
-
return Download(
|
|
340
|
+
return Download(build_client_opener().open(target, timeout=self.timeout))
|
|
313
341
|
except HTTPError as error:
|
|
314
342
|
error.close()
|
|
315
343
|
raise RemoteError("ARTIFACT_DOWNLOAD_FAILED", error.code) from None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|