envloop-cli 1.29.0__tar.gz → 1.30.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.
Files changed (64) hide show
  1. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/PKG-INFO +3 -3
  2. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/README.pypi.md +2 -2
  3. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/auth_cli.py +3 -0
  4. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/auth_login.py +4 -1
  5. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/cli_errors.py +7 -3
  6. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/lifecycle_cli.py +3 -1
  7. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/data_cli.py +6 -2
  8. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/resource_cli.py +7 -6
  9. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/cli.py +8 -1
  10. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/train_log_cli.py +4 -1
  11. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/train_metrics_cli.py +4 -3
  12. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/pyproject.toml +2 -2
  13. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/.gitignore +0 -0
  14. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/__init__.py +0 -0
  15. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/_optional.py +0 -0
  16. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/auth_workspace.py +0 -0
  17. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/cli.py +0 -0
  18. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/cli_output.py +0 -0
  19. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/client.py +0 -0
  20. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/config.py +0 -0
  21. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/downloads.py +0 -0
  22. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/http.py +0 -0
  23. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/navigation_resources.py +0 -0
  24. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/profile_secrets.py +0 -0
  25. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/resource_cli.py +0 -0
  26. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/resources.py +0 -0
  27. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/terminal.py +0 -0
  28. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/utils/__init__.py +0 -0
  29. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/envloop/utils/logging.py +0 -0
  30. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/__init__.py +0 -0
  31. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/cli.py +0 -0
  32. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/cli_output.py +0 -0
  33. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/client.py +0 -0
  34. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/data.py +0 -0
  35. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/data_publications.py +0 -0
  36. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/data_upload.py +0 -0
  37. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/http.py +0 -0
  38. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/navigation_resources.py +0 -0
  39. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/profile_updates.py +0 -0
  40. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/recipes.py +0 -0
  41. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/data/envloop_data/runner_packages.py +0 -0
  42. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/__init__.py +0 -0
  43. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/cli_output.py +0 -0
  44. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/client.py +0 -0
  45. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/decision.py +0 -0
  46. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/http.py +0 -0
  47. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/inference.py +0 -0
  48. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/job_cost.py +0 -0
  49. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/job_logs.py +0 -0
  50. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/job_spec_cli.py +0 -0
  51. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/navigation_resources.py +0 -0
  52. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/resource_cli.py +0 -0
  53. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/resources.py +0 -0
  54. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/runtime_params.py +0 -0
  55. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/sandbox.py +0 -0
  56. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/train.py +0 -0
  57. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/train_progress.py +0 -0
  58. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/train_runner_cli.py +0 -0
  59. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/experiment/envloop_experiment/training/__init__.py +0 -0
  60. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/rsi-loop/envloop_rsi_loop/__init__.py +0 -0
  61. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/rsi-loop/envloop_rsi_loop/agent_runs.py +0 -0
  62. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/rsi-loop/envloop_rsi_loop/cli.py +0 -0
  63. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/rsi-loop/envloop_rsi_loop/cli_output.py +0 -0
  64. {envloop_cli-1.29.0 → envloop_cli-1.30.0}/packages/rsi-loop/envloop_rsi_loop/rsi_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: envloop-cli
3
- Version: 1.29.0
3
+ Version: 1.30.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
@@ -249,7 +249,7 @@ The platform starts the published Revision’s `main.sh` / `main.py` using the R
249
249
  CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
250
250
  Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
251
251
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
252
- List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
252
+ List and table times are UTC with a `UTC` label; Job logs retain the recorded timezone and mark lines collected only after the Job ended with `(backfilled)`. Full precision, offsets and the `backfilled` field are available through `--json`.
253
253
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
254
254
 
255
255
  ## Job-bound decision plugins
@@ -283,7 +283,7 @@ download a complete registered ANS object directory through EnvHub. Datasets req
283
283
  authorization the SDK uses GET URLs valid for up to five minutes, verifies file size/SHA-256,
284
284
  and installs the directory atomically. Matching platform support is required.
285
285
 
286
- A compatible server and an explicitly granted project Key with `envloop:data:read` are required; writes additionally need `envloop:data:write`. Default and older Keys do not gain these permissions. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
286
+ A compatible server is required. Data uses the permission of the resource it touches: read (for example `datasets.read`) to query and download, write to publish and upload, delete to retire; a key from `el auth login` follows your project role. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
287
287
 
288
288
  ```bash
289
289
  el data list --type dataset --json
@@ -232,7 +232,7 @@ The platform starts the published Revision’s `main.sh` / `main.py` using the R
232
232
  CLI output uses compact timestamps and highlights statuses and log levels by default on terminals.
233
233
  Use `el --no-color job list` or append `--no-color` to a command to disable highlighting, including errors.
234
234
  `NO_COLOR=1`, `TERM=dumb`, pipes, and redirected output also disable colors. JSON preserves original values.
235
- List times use the local timezone; Job logs retain the recorded timezone, with full precision and offsets available through `--json`.
235
+ List and table times are UTC with a `UTC` label; Job logs retain the recorded timezone and mark lines collected only after the Job ended with `(backfilled)`. Full precision, offsets and the `backfilled` field are available through `--json`.
236
236
  `el job run live RUN --follow --json` emits complete response frames as NDJSON; Ctrl-C stops reading without stopping the Run.
237
237
 
238
238
  ## Job-bound decision plugins
@@ -266,7 +266,7 @@ download a complete registered ANS object directory through EnvHub. Datasets req
266
266
  authorization the SDK uses GET URLs valid for up to five minutes, verifies file size/SHA-256,
267
267
  and installs the directory atomically. Matching platform support is required.
268
268
 
269
- A compatible server and an explicitly granted project Key with `envloop:data:read` are required; writes additionally need `envloop:data:write`. Default and older Keys do not gain these permissions. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
269
+ A compatible server is required. Data uses the permission of the resource it touches: read (for example `datasets.read`) to query and download, write to publish and upload, delete to retire; a key from `el auth login` follows your project role. The current login selects the base URL, workspace and project; there is no `--env` flag. After configuring the Key using `el auth config`:
270
270
 
271
271
  ```bash
272
272
  el data list --type dataset --json
@@ -20,6 +20,9 @@ def add_auth_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParse
20
20
  login.add_argument("--no-browser", action="store_true", help="Print the login link without opening a browser")
21
21
  login.add_argument("--timeout-seconds", type=int, choices=range(1, 601), metavar="SECONDS", default=300,
22
22
  help="Wait up to 1–600 seconds (default: 300)")
23
+ login.add_argument("--read-only", action="store_true",
24
+ help="Issue a read-only key, for example for CI that only reads results. "
25
+ "Without it the key follows your project role")
23
26
  configure = commands.add_parser("config", help="Save Base URL and API key for CLI and SDK")
24
27
  configure.add_argument("--no-check", action="store_true", help="Save without API verification (offline; key remains unverified)")
25
28
  actions = configure.add_subparsers(dest="config_action")
@@ -76,7 +76,8 @@ def login(args: argparse.Namespace, store: Any) -> int:
76
76
  def digest(value: str) -> str:
77
77
  return hashlib.sha256(value.encode()).hexdigest()
78
78
  response = _request(url, 'start', {'key_hash': digest(key), 'poll_hash': digest(poll_token),
79
- 'code_hash': digest(code)}, min(5, args.timeout_seconds))
79
+ 'code_hash': digest(code), **({'read_only': True} if args.read_only else {})},
80
+ min(5, args.timeout_seconds))
80
81
  identifier, uri = _verification_url(url, response)
81
82
  interval, expires = response.get('interval'), response.get('expires_in')
82
83
  if (isinstance(interval, bool) or not isinstance(interval, (int, float)) or not 0 < interval <= 10 or
@@ -88,6 +89,8 @@ def login(args: argparse.Namespace, store: Any) -> int:
88
89
  try:
89
90
  print(f'Base URL: {url}\nConfirmation code: {code}\nOpen: {uri}', flush=True)
90
91
  print('Sign in with Clerk, enter this code, and authorize your workspace/project.', flush=True)
92
+ print('The key will be read only.' if args.read_only else
93
+ 'The key follows your project role. The page lists its permissions before you authorize.', flush=True)
91
94
  if not args.no_browser:
92
95
  try:
93
96
  opened = webbrowser.open(uri, new=1)
@@ -27,10 +27,10 @@ def _register(codes: str, message: str) -> None:
27
27
  _MESSAGES[code] = message
28
28
 
29
29
 
30
- _register("DATA_SCOPE_REQUIRED", "Configure a project key with explicit Data read permission, plus Data write for publication and upload management.")
30
+ _register("DATA_ACCESS_REQUIRED", "This key lacks the permission named above on the Dataset, Runner, Profile, Checkpoint or Trial artifact. Run el auth login to issue a new key that follows your project role: every role can read, and Developer or Admin can also write and delete (create, update, delete, upload, runner register). A key from el auth login --read-only cannot write; log in again without --read-only. Or give a key Read or Edit on that page in the Console under CONFIGURE > API Keys. Existing keys never gain permissions.")
31
31
  _register("DATA_NATIVE_INCOMPLETE DATA_NATIVE_UNAVAILABLE", "Native catalog registration is incomplete. Retry the same input after the platform is ready.")
32
32
  _register("DATA_DEPENDENCIES_ACTIVE DATA_DEPENDENCIES_UNKNOWN DATA_DEPENDENCIES_CHANGED DATA_DELETE_CONFLICT", "Retirement requires a complete, current dependency check. Resolve active or unknown dependencies, then retry the same resource.")
33
- _register("DATA_PUBLICATION_INVALID", "Check the immutable ANS URI, native manifest and exact file sizes and SHA-256 values.")
33
+ _register("DATA_PUBLICATION_INVALID", "Check the immutable ANS URI, native manifest and exact file sizes and SHA-256 values. A record Dataset source is https://github.com/OWNER/REPO or https://huggingface.co/datasets/ORG/NAME with the full commit, not a branch or tag.")
34
34
  _register("DATA_PIN_CONFLICT DATA_REVISION_REQUIRED", "Publish a new immutable revision; existing and revoked references cannot be overwritten.")
35
35
  _register("DATA_QUOTA_EXCEEDED DATA_QUOTA_UNCONFIGURED", "Ask a project administrator to configure publication storage quota. Revoking publications does not free retained content.")
36
36
  _register("DATA_UPLOAD_CONFLICT DATA_UPLOAD_INCOMPLETE DATA_UPLOAD_RATE_LIMITED", "Inspect the upload with data upload get; resume matching parts or explicitly abort it.")
@@ -329,7 +329,11 @@ def upload_recovery(error: BaseException) -> str:
329
329
  if isinstance(upload_id, str) and re.fullmatch(r'upload_[a-f0-9]{32}', upload_id):
330
330
  lines.extend([f' Upload retained: {upload_id}', f' Inspect with: el data upload get {upload_id}'])
331
331
  generated = getattr(error, 'generated_idempotency_key', None)
332
- if isinstance(generated, str) and re.fullmatch(r'[a-f0-9]{32}', generated):
332
+ # A definite refusal (permission, validation, not found) fails the same way again; offer the key only
333
+ # when the outcome is unknown or retryable.
334
+ status = getattr(error, 'status_code', None)
335
+ retryable = not isinstance(status, int) or status >= 500 or status in {408, 429}
336
+ if retryable and isinstance(generated, str) and re.fullmatch(r'[a-f0-9]{32}', generated):
333
337
  lines.append(f' Retry the same input with --idempotency-key {generated}')
334
338
  return '\n'.join(lines)
335
339
 
@@ -18,7 +18,9 @@ def add_management_commands(settings: "argparse._SubParsersAction[argparse.Argum
18
18
  if action == 'wait':
19
19
  command.add_argument('--timeout-seconds', type=float, default=600)
20
20
  cleanup = settings.add_parser('cleanup', help='Clean an explicit set of Project resource IDs')
21
- cleanup.add_argument('--file', type=Path, required=True, help='JSON array of resource kind and ID objects')
21
+ cleanup.add_argument('--file', type=Path, required=True,
22
+ help='JSON array of {"kind": KIND, "id": ID} objects; KIND is job (a Train Job), endpoint, adapter '
23
+ 'or checkpoint, for example [{"kind": "job", "id": "job_..."}]')
22
24
  cleanup.add_argument('--request-id', required=True)
23
25
  wait_options(cleanup)
24
26
  scopes = settings.add_parser('resource-scope', help='Manage temporary resources with server TTL').add_subparsers(dest='scope_action', required=True)
@@ -83,15 +83,19 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]",
83
83
  command.add_argument('--json', action='store_true')
84
84
 
85
85
 
86
+ MANIFEST_HELP = ('Native JSON content manifest; a record Dataset source.repository is https://github.com/OWNER/REPO '
87
+ 'or https://huggingface.co/datasets/ORG/NAME, with source.commit the full commit')
88
+
89
+
86
90
  def publication_arguments(command: argparse.ArgumentParser, *, source: bool = False):
87
91
  command.add_argument('--ref', required=not source, help='Exact immutable ANS URI')
88
92
  if source:
89
93
  inputs = command.add_mutually_exclusive_group(required=True)
90
- inputs.add_argument('--manifest', type=Path, help='Native JSON content manifest')
94
+ inputs.add_argument('--manifest', type=Path, help=MANIFEST_HELP)
91
95
  inputs.add_argument('--model', help='Public Hugging Face model namespace/name; metadata only')
92
96
  command.add_argument('--revision', help='Explicit HF source ref; the server freezes its full commit')
93
97
  else:
94
- command.add_argument('--manifest', type=Path, required=True, help='Native JSON content manifest')
98
+ command.add_argument('--manifest', type=Path, required=True, help=MANIFEST_HELP)
95
99
  command.add_argument('--directory', type=Path, required=not source, help='Directory containing exactly the declared files')
96
100
  command.add_argument('--executable', action='append', dest='executables', default=[],
97
101
  help='Declared Runner file that must be executable; repeat for each path')
@@ -33,8 +33,8 @@ def add_resource_commands(groups: "argparse._SubParsersAction[argparse.ArgumentP
33
33
  'immutable Runner revision NAME@REVISION. The platform stores the bytes, computes the package and index '
34
34
  'SHA-256, creates the Runner on first use and registers the revision; identical retries are idempotent and '
35
35
  'changed content for an existing revision is refused. This is el data create --type runner with the native '
36
- 'manifest built for you; use that command to publish an existing manifest. The key needs envloop:data:write '
37
- 'and runners.write, and publish permission on a Runner owned by another Project.')
36
+ 'manifest built for you; use that command to publish an existing manifest. The key needs runners.write '
37
+ '(el auth login issues it to a Developer or Admin), and publish permission on a Runner owned by another Project.')
38
38
  register.add_argument('directory', metavar='DIRECTORY', type=Path, help='Runner package directory; every file in it is published')
39
39
  register.add_argument('--revision', required=True, help='Full 40- or 64-character source commit, for example $(git rev-parse HEAD)')
40
40
  register.add_argument('--name', help='NAMESPACE/NAME; must equal [runner] name in runner.toml (default: that name)')
@@ -88,9 +88,10 @@ def execute(args: argparse.Namespace, client: Any):
88
88
  cursor=args.cursor, limit=args.limit)
89
89
 
90
90
 
91
- RUNNER_WRITE_HINT = ('Registering a Runner revision needs a project key with Data write (envloop:data:write) and '
92
- 'Runners write (runners.write), and publish or admin permission when another project owns the '
93
- 'Runner. Existing keys keep their original capabilities; issue a new key with these permissions.')
91
+ RUNNER_WRITE_HINT = ('Registering a Runner revision needs a project key with Runners write (runners.write), and '
92
+ 'publish or admin permission when another project owns the Runner. Existing keys keep their '
93
+ 'original capabilities; run el auth login without --read-only (Developer or Admin role) to issue '
94
+ 'a new key with it.')
94
95
 
95
96
 
96
97
  def register_runner(client: Any, args: argparse.Namespace):
@@ -102,7 +103,7 @@ def register_runner(client: Any, args: argparse.Namespace):
102
103
  executables=args.executables, source_ref=args.source_ref,
103
104
  source_ref_kind=args.source_ref_kind, dry_run=args.dry_run)
104
105
  except RemoteError as error:
105
- if error.code == 'ACCESS_DENIED' and not error.hint:
106
+ if error.code in {'ACCESS_DENIED', 'DATA_ACCESS_REQUIRED'} and not error.hint:
106
107
  error.hint = RUNNER_WRITE_HINT
107
108
  raise
108
109
  if not args.dry_run:
@@ -5,6 +5,9 @@ import sys
5
5
  from pathlib import Path
6
6
  from typing import Any
7
7
 
8
+ # Single-resource reads and actions print the response as JSON already; scripts may still pass --json.
9
+ JSON_DEFAULT_HELP = "Output the complete response as JSON (the default; accepted for consistency with list commands)"
10
+
8
11
 
9
12
  def dataset_reference(value: str):
10
13
  name, separator, version = value.rpartition("@")
@@ -155,6 +158,8 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
155
158
  if action == 'event':
156
159
  command.add_argument('--json', action='store_true')
157
160
  command.set_defaults(_list_view=True)
161
+ else:
162
+ command.add_argument("--json", action="store_true", help=JSON_DEFAULT_HELP)
158
163
  regrade = jobs.add_parser("regrade", help="Re-run only the verifier of sealed compose Trials, optionally with a reward set",
159
164
  allow_abbrev=False)
160
165
  regrade_source = regrade.add_mutually_exclusive_group(required=True)
@@ -218,7 +223,9 @@ def add_commands(groups: "argparse._SubParsersAction[argparse.ArgumentParser]"):
218
223
  snapshot = tasks.add_parser("snapshot")
219
224
  snapshot.add_argument("trial_id", metavar="TRIAL_ID")
220
225
  for action in ("get", "stop", "result"):
221
- tasks.add_parser(action).add_argument("trial_id", metavar="TRIAL_ID")
226
+ command = tasks.add_parser(action)
227
+ command.add_argument("trial_id", metavar="TRIAL_ID")
228
+ command.add_argument("--json", action="store_true", help=JSON_DEFAULT_HELP)
222
229
  listing.add_argument("--json", action="store_true", help="Output the complete response as JSON")
223
230
  artifacts = tasks.add_parser("artifact", help="List or download a Trial's artifacts").add_subparsers(dest="artifact_action", required=True)
224
231
  artifact_list = artifacts.add_parser("list", help="List one page of artifacts")
@@ -30,10 +30,13 @@ def log_line(item: dict[str, Any], *, color: bool = False):
30
30
  if item.get("phase"):
31
31
  diagnostic += f"; phase={safe_text(item['phase'])}"
32
32
  message = f"[{diagnostic}] {message}"
33
+ # The line had no write time and was read after the Job ended: its time is when it was collected.
34
+ backfilled = "(backfilled)" if item.get("backfilled") is True else ""
33
35
  if color:
34
36
  timestamp = style(timestamp, "2") if timestamp else ""
37
+ backfilled = style(backfilled, "2") if backfilled else ""
35
38
  level = status_text(level)
36
- prefix = " ".join(part for part in (timestamp, level) if part)
39
+ prefix = " ".join(part for part in (timestamp, backfilled, level) if part)
37
40
  return f"{prefix} {message}"
38
41
 
39
42
 
@@ -6,7 +6,7 @@ import math
6
6
  import re
7
7
  import sys
8
8
  import time
9
- from datetime import datetime
9
+ from datetime import datetime, timezone
10
10
  from typing import Any
11
11
 
12
12
  from envloop.cli_output import display, table
@@ -20,7 +20,7 @@ FOLLOW_RETRIES = 3
20
20
  TERMINAL = {"completed", "succeeded", "failed", "cancelled", "SUCCEEDED", "FAILED", "CANCELLED"}
21
21
  HEADERS = ("STEP", "LOSS", "LEARNING RATE", "GRAD NORM", "EPOCH", "TOKENS", "SECONDS/STEP", "OBSERVED AT")
22
22
  # Column widths of --follow, which prints rows before later ones are known.
23
- WIDTHS = (6, 10, 13, 10, 8, 12, 12, 19)
23
+ WIDTHS = (6, 10, 13, 10, 8, 12, 12, 23)
24
24
 
25
25
 
26
26
  def _number(value: Any) -> float | None:
@@ -41,8 +41,9 @@ def _value(value: Any, spec: str) -> str | None:
41
41
 
42
42
 
43
43
  def _observed(value: Any) -> str | None:
44
+ """``at`` is Unix time; like every table time it is shown in UTC, labelled (--json keeps the number)."""
44
45
  number = _number(value)
45
- return None if number is None else datetime.fromtimestamp(number).strftime("%Y-%m-%d %H:%M:%S")
46
+ return None if number is None else datetime.fromtimestamp(number, timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
46
47
 
47
48
 
48
49
  def step_row(point: dict[str, Any], *, model: bool = False) -> tuple[object, ...]:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "envloop-cli"
3
- version = "1.29.0"
3
+ version = "1.30.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"
@@ -65,7 +65,7 @@ combine-as-imports = true
65
65
  known-first-party = ["envloop", "envloop_data", "envloop_experiment", "envloop_rsi_loop"]
66
66
 
67
67
  [tool.uv.sources]
68
- envloop-envhub = { git = "ssh://git@github.com/EnvLoop/EnvHub.git", rev = "8c96b155d7ab0153da6359508a40c5a4fe802e78" }
68
+ envloop-envhub = { git = "ssh://git@github.com/EnvLoop/EnvHub.git", rev = "db09a00dc6cc2962e599b3c8adbab320e647b578" }
69
69
 
70
70
  [tool.pyrefly]
71
71
  preset = "strict"
File without changes