envloop-cli 1.23.0__tar.gz → 1.24.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.23.0 → envloop_cli-1.24.0}/PKG-INFO +3 -3
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/cli_errors.py +10 -2
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/cli_output.py +19 -3
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/client.py +2 -1
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/http.py +7 -1
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/navigation_resources.py +8 -1
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/pyproject.toml +4 -4
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/.gitignore +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/README.pypi.md +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/__init__.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/_optional.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/auth_cli.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/auth_login.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/auth_workspace.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/cli.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/config.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/downloads.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/lifecycle_cli.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/profile_secrets.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/resource_cli.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/resources.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/terminal.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.0}/envloop/utils/__init__.py +0 -0
- {envloop_cli-1.23.0 → envloop_cli-1.24.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.24.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
|
|
@@ -8,12 +8,12 @@ Project-URL: Repository, https://github.com/EnvLoop/envloop-cli
|
|
|
8
8
|
Project-URL: Issues, https://github.com/EnvLoop/envloop-cli/issues
|
|
9
9
|
Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
|
-
Requires-Dist: envloop-cli-experiment==1.
|
|
11
|
+
Requires-Dist: envloop-cli-experiment==1.24.0
|
|
12
12
|
Provides-Extra: data
|
|
13
13
|
Requires-Dist: envloop-cli-data==1.23.0; extra == 'data'
|
|
14
14
|
Provides-Extra: default
|
|
15
15
|
Provides-Extra: experiment
|
|
16
|
-
Requires-Dist: envloop-cli-experiment==1.
|
|
16
|
+
Requires-Dist: envloop-cli-experiment==1.24.0; extra == 'experiment'
|
|
17
17
|
Provides-Extra: rsi-loop
|
|
18
18
|
Requires-Dist: envloop-cli-rsi-loop==1.23.0; extra == 'rsi-loop'
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
@@ -17,7 +17,7 @@ from collections.abc import Iterable
|
|
|
17
17
|
from datetime import datetime
|
|
18
18
|
from typing import Any, cast
|
|
19
19
|
|
|
20
|
-
from .http import RemoteError
|
|
20
|
+
from .http import GATEWAY_TIMEOUT_STATUSES, RemoteError
|
|
21
21
|
|
|
22
22
|
_MESSAGES: dict[str, str] = {}
|
|
23
23
|
|
|
@@ -54,6 +54,7 @@ _register("RATE_LIMITED RATE_LIMIT_EXCEEDED TOO_MANY_REQUESTS", "The API rate li
|
|
|
54
54
|
_register("SERVICE_UNAVAILABLE INTERNAL_SERVER_ERROR EXECUTOR_UNAVAILABLE PROVIDER_UNAVAILABLE LOG_STORAGE_UNAVAILABLE", "The service is unavailable or encountered an internal error.")
|
|
55
55
|
_register("INTERNAL_ERROR", "The command encountered an unexpected error.")
|
|
56
56
|
_register("NETWORK_ERROR", "A connection to the API or download service could not be completed.")
|
|
57
|
+
_register("SOURCE_UNAVAILABLE", "The API gateway did not get a valid answer from the service, for example because it stopped waiting. A write may still have been processed: read the Job or resource before retrying it.")
|
|
57
58
|
_register("REQUEST_TIMEOUT", "The API did not answer before the client timeout. The request may still have been processed: read the Job or resource before retrying a write.")
|
|
58
59
|
_register("RESPONSE_INTERRUPTED", "The connection closed before the API finished answering; the service may have failed or restarted. The request may have been processed: read the Job or resource before retrying a write, and report the time if it repeats.")
|
|
59
60
|
_register("INVALID_RESPONSE RESPONSE_TOO_LARGE INVALID_LOG_PAGE INVALID_PAGINATION TRAIN_JOB_CURSOR_REPEATED", "The service returned an invalid or unsupported response.")
|
|
@@ -114,6 +115,8 @@ _register("INVALID_RSI_REQUEST INVALID_STEER INVALID_DIRECTIVE", "The RSI servic
|
|
|
114
115
|
_register("RSI_CONFLICT RSI_RUN_NOT_ACTIVE", "The run id exists, the run is not in a state that allows this action, or no loop machine is free.")
|
|
115
116
|
_register("RSI_CONTROL_UNCONFIGURED RSI_CONTROL_UNAVAILABLE RSI_CONTROL_TIMEOUT", "The RSI loop service is not available through this platform right now.")
|
|
116
117
|
|
|
118
|
+
_register("UNSUPPORTED_ADAPTER_SOURCE", 'Register a training checkpoint with {"serving_profile": "NAME", "source": {"type": "train_checkpoint", "tinker_path": "envloop://..."}}. '
|
|
119
|
+
"List serving profiles with `el job serving-profile list` and checkpoint paths with `el checkpoint list`.")
|
|
117
120
|
_register("JOB_ACTION_UNSUPPORTED", "This Job type does not support the requested action.")
|
|
118
121
|
_register("INFERENCE_ROUTE_RETIRED", "The global inference route has been retired.")
|
|
119
122
|
|
|
@@ -209,6 +212,9 @@ def _http_message(status: int | None) -> str:
|
|
|
209
212
|
return _MESSAGES["INVALID_REQUEST"]
|
|
210
213
|
if status == 429:
|
|
211
214
|
return _MESSAGES["RATE_LIMITED"]
|
|
215
|
+
if status in GATEWAY_TIMEOUT_STATUSES:
|
|
216
|
+
return ("The API gateway stopped waiting for the service. The request may still have been processed: "
|
|
217
|
+
"read the Job or resource before retrying a write.")
|
|
212
218
|
if status is not None and status >= 500:
|
|
213
219
|
return _MESSAGES["SERVICE_UNAVAILABLE"]
|
|
214
220
|
return _MESSAGES["HTTP_ERROR"]
|
|
@@ -294,7 +300,9 @@ def format_error(error: BaseException) -> str:
|
|
|
294
300
|
extra: dict[str, Any] = {"hint": remote.hint, "details": remote.details, "request_id": remote.request_id} if remote else {}
|
|
295
301
|
if remote and remote.message:
|
|
296
302
|
# Local text only adds guidance for codes the CLI documents.
|
|
297
|
-
|
|
303
|
+
# A gateway timeout always adds that the request may have been processed.
|
|
304
|
+
documented = (code in _MESSAGES and code != "HTTP_ERROR") or status in GATEWAY_TIMEOUT_STATUSES
|
|
305
|
+
guidance = local if documented and local != remote.message else None
|
|
298
306
|
rendered = error_block(code, remote.message, status=status, guidance=guidance, **extra)
|
|
299
307
|
else:
|
|
300
308
|
rendered = error_block(code, local, status=status, **extra)
|
|
@@ -59,15 +59,31 @@ def next_page(args: argparse.Namespace, cursor: object, program: str) -> str:
|
|
|
59
59
|
return "Next page: " + shlex.join(command)
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
# Empty-list text names the listed resource, not the command group (`job adapter list` lists adapters).
|
|
63
|
+
EMPTY_RESOURCES = {
|
|
64
|
+
('job', 'adapter'): 'adapters', ('job', 'serving-profile'): 'serving profiles',
|
|
65
|
+
('trail',): 'trails', ('trail', 'artifact'): 'artifacts',
|
|
66
|
+
('dataset',): 'datasets', ('dataset', 'revision'): 'dataset revisions', ('dataset', 'task'): 'dataset tasks',
|
|
67
|
+
('data',): 'resources', ('data', 'files'): 'files',
|
|
68
|
+
('billing', 'usage'): 'usage records', ('billing', 'ledger'): 'ledger entries', ('billing', 'price'): 'prices',
|
|
69
|
+
('member',): 'members', ('member', 'candidate'): 'member candidates',
|
|
70
|
+
('setting', 'registry'): 'registry credentials',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def empty_message(args: argparse.Namespace) -> str:
|
|
75
|
+
path = getattr(args, '_command_path', [args.group, 'list'])
|
|
76
|
+
resource = EMPTY_RESOURCES.get(tuple(path[:-1] if path[-1:] == ['list'] else path))
|
|
77
|
+
return f'No {resource or args.group} found.'
|
|
78
|
+
|
|
79
|
+
|
|
62
80
|
def generic_layout(items: list[dict[str, Any]], args: argparse.Namespace, layouts: dict[str, tuple[str, ...]]) -> tuple[tuple[str, ...], Callable[[dict[str, Any]], tuple[object, ...]], str]:
|
|
63
81
|
fields = tuple(field for field in layouts.get(args.group, ()) if any(field in item for item in items))
|
|
64
82
|
fields = fields or tuple(dict.fromkeys(key for item in items for key in item))[:6]
|
|
65
83
|
headers = tuple(field.replace('_', ' ').upper() for field in fields)
|
|
66
84
|
render: Callable[[dict[str, Any]], tuple[object, ...]] = lambda item: tuple(timestamp(item.get(field)) if field.endswith('_at') or field == 'at'
|
|
67
85
|
else item.get(field) for field in fields)
|
|
68
|
-
|
|
69
|
-
else f'No {getattr(args, "resource", args.group)} found.')
|
|
70
|
-
return headers, render, empty
|
|
86
|
+
return headers, render, empty_message(args)
|
|
71
87
|
|
|
72
88
|
|
|
73
89
|
def list_output(result: Any, args: argparse.Namespace, program: str) -> str:
|
|
@@ -33,9 +33,10 @@ class Client:
|
|
|
33
33
|
raise RuntimeError(INTERACTIVE_TRAINING_REMOVED)
|
|
34
34
|
|
|
35
35
|
@classmethod
|
|
36
|
-
def remote(cls, api_url: str | None = None, *, timeout: float =
|
|
36
|
+
def remote(cls, api_url: str | None = None, *, timeout: float = 90, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True):
|
|
37
37
|
from .http import HttpService
|
|
38
38
|
|
|
39
|
+
# The default matches http.DEFAULT_TIMEOUT_SEC (longer than the hosted proxy's 60 s wait).
|
|
39
40
|
return cls(HttpService(api_url, timeout, api_key=api_key, workspace_id=workspace_id, project_id=project_id, config=config))
|
|
40
41
|
|
|
41
42
|
@classmethod
|
|
@@ -63,6 +63,12 @@ def retry_after_seconds(value: str | None) -> float | None:
|
|
|
63
63
|
return None
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
# The hosted API proxy waits up to 60 s for the platform. Wait longer by default so a slow answer, or the
|
|
67
|
+
# proxy's own gateway timeout, reaches the caller instead of a client-side timeout. Explicit values are kept.
|
|
68
|
+
DEFAULT_TIMEOUT_SEC = 90.0
|
|
69
|
+
# The proxy or edge stopped waiting while the platform was still working: the request may have been processed.
|
|
70
|
+
GATEWAY_TIMEOUT_STATUSES = frozenset({504, 524})
|
|
71
|
+
|
|
66
72
|
# Transport failures that are not an HTTP answer. A request that was sent and then timed out or lost its
|
|
67
73
|
# response may still have been processed, unlike one that never connected.
|
|
68
74
|
TRANSIENT_CODES = frozenset({"NETWORK_ERROR", "REQUEST_TIMEOUT", "RESPONSE_INTERRUPTED"})
|
|
@@ -213,7 +219,7 @@ def encode_json(body: Any) -> bytes:
|
|
|
213
219
|
|
|
214
220
|
|
|
215
221
|
class HttpService:
|
|
216
|
-
def __init__(self, base_url: str | None = None, timeout: float =
|
|
222
|
+
def __init__(self, base_url: str | None = None, timeout: float = DEFAULT_TIMEOUT_SEC, *, api_key: str | None = None, workspace_id: str | None = None, project_id: str | None = None, config: bool = True, environment: bool = True):
|
|
217
223
|
from .config import require_remote, resolve_connection
|
|
218
224
|
|
|
219
225
|
connection = resolve_connection(base_url, api_key, workspace_id, project_id, config=config, environment=environment)
|
|
@@ -33,7 +33,14 @@ class Billing:
|
|
|
33
33
|
start, end = billing_timestamp(start), billing_timestamp(end)
|
|
34
34
|
if datetime.fromisoformat(end) <= datetime.fromisoformat(start):
|
|
35
35
|
raise ValueError('billing end must be after start')
|
|
36
|
-
|
|
36
|
+
# Workspace usage needs a human billing grant. An API key reads its own project's usage on the
|
|
37
|
+
# project route, which takes the same window and grouping.
|
|
38
|
+
identity = self.transport.request('GET', '/v1/me')
|
|
39
|
+
identity = identity if isinstance(identity, dict) else {}
|
|
40
|
+
project = identity.get('project_id') or (identity.get('project') or {}).get('project_id')
|
|
41
|
+
path = (f'/v1/projects/{quote(project, safe="")}/billing/usage'
|
|
42
|
+
if identity.get('auth_method') == 'api_key' and isinstance(project, str) and project else '/v1/billing/usage')
|
|
43
|
+
return self.transport.request('GET', query(path, start=start, end=end, group_by=group_by))
|
|
37
44
|
|
|
38
45
|
def ledger(self, *, limit: int = 25):
|
|
39
46
|
return self.transport.request('GET', query('/v1/billing/ledger', limit=limit))
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "envloop-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.24.0"
|
|
4
4
|
description = "EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference"
|
|
5
5
|
requires-python = ">=3.12"
|
|
6
6
|
readme = "README.pypi.md"
|
|
7
|
-
dependencies = ["envloop-cli-experiment==1.
|
|
7
|
+
dependencies = ["envloop-cli-experiment==1.24.0"]
|
|
8
8
|
|
|
9
9
|
[project.optional-dependencies]
|
|
10
10
|
default = []
|
|
11
11
|
data = ["envloop-cli-data==1.23.0"]
|
|
12
|
-
experiment = ["envloop-cli-experiment==1.
|
|
12
|
+
experiment = ["envloop-cli-experiment==1.24.0"]
|
|
13
13
|
rsi-loop = ["envloop-cli-rsi-loop==1.23.0"]
|
|
14
14
|
|
|
15
15
|
[project.scripts]
|
|
@@ -24,7 +24,7 @@ Issues = "https://github.com/EnvLoop/envloop-cli/issues"
|
|
|
24
24
|
"Agent instructions" = "https://docs.envloop.ai/skill.md"
|
|
25
25
|
|
|
26
26
|
[dependency-groups]
|
|
27
|
-
dev = ["ruff==0.16.10", "pre-commit>=4.6,<5", "pytest>=8", "envloop-cli-data==1.23.0", "envloop-cli-experiment==1.
|
|
27
|
+
dev = ["ruff==0.16.10", "pre-commit>=4.6,<5", "pytest>=8", "envloop-cli-data==1.23.0", "envloop-cli-experiment==1.24.0", "envloop-cli-rsi-loop==1.23.0", "pyrefly==1.4.0"]
|
|
28
28
|
|
|
29
29
|
[build-system]
|
|
30
30
|
requires = ["hatchling"]
|
|
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
|