envloop-cli 0.1.1__tar.gz → 0.2.1__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 (39) hide show
  1. envloop_cli-0.2.1/.gitignore +8 -0
  2. envloop_cli-0.2.1/PKG-INFO +98 -0
  3. envloop_cli-0.2.1/README.pypi.md +87 -0
  4. envloop_cli-0.2.1/envloop/__init__.py +5 -0
  5. envloop_cli-0.2.1/envloop/agent_runs.py +208 -0
  6. envloop_cli-0.2.1/envloop/auth_cli.py +285 -0
  7. envloop_cli-0.2.1/envloop/auth_login.py +127 -0
  8. envloop_cli-0.2.1/envloop/auth_workspace.py +290 -0
  9. envloop_cli-0.2.1/envloop/cli.py +452 -0
  10. envloop_cli-0.2.1/envloop/cli_output.py +206 -0
  11. envloop_cli-0.2.1/envloop/client.py +548 -0
  12. envloop_cli-0.2.1/envloop/config.py +440 -0
  13. envloop_cli-0.2.1/envloop/http.py +274 -0
  14. envloop_cli-0.2.1/envloop/lifecycle_cli.py +53 -0
  15. envloop_cli-0.2.1/envloop/navigation_resources.py +260 -0
  16. envloop_cli-0.2.1/envloop/resource_cli.py +164 -0
  17. envloop_cli-0.2.1/envloop/resources.py +210 -0
  18. envloop_cli-0.2.1/envloop/train.py +51 -0
  19. envloop_cli-0.2.1/envloop/training/__init__.py +120 -0
  20. envloop_cli-0.2.1/envloop/training/credentials.py +56 -0
  21. envloop_cli-0.2.1/envloop/training/service_client.py +40 -0
  22. envloop_cli-0.2.1/pyproject.toml +57 -0
  23. envloop_cli-0.1.1/.gitignore +0 -5
  24. envloop_cli-0.1.1/PKG-INFO +0 -93
  25. envloop_cli-0.1.1/README.md +0 -85
  26. envloop_cli-0.1.1/pyproject.toml +0 -35
  27. envloop_cli-0.1.1/src/el_cli/__init__.py +0 -1
  28. envloop_cli-0.1.1/src/el_cli/cli.py +0 -257
  29. envloop_cli-0.1.1/src/el_cli/client.py +0 -335
  30. envloop_cli-0.1.1/src/el_cli/http.py +0 -126
  31. envloop_cli-0.1.1/src/el_cli/train.py +0 -53
  32. envloop_cli-0.1.1/tests/test_bearer.py +0 -117
  33. envloop_cli-0.1.1/tests/test_cli_help.py +0 -98
  34. envloop_cli-0.1.1/tests/test_http_proxy.py +0 -59
  35. envloop_cli-0.1.1/tests/test_job_submit_cli.py +0 -50
  36. envloop_cli-0.1.1/tests/test_jobs_http.py +0 -183
  37. envloop_cli-0.1.1/tests/test_train_cli.py +0 -42
  38. {envloop_cli-0.1.1/src/el_cli → envloop_cli-0.2.1/envloop}/utils/__init__.py +0 -0
  39. {envloop_cli-0.1.1/src/el_cli → envloop_cli-0.2.1/envloop}/utils/logging.py +0 -0
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ .pytest_cache/
4
+ dist/
5
+ /logs/
6
+
7
+ # Local environment configuration is machine-specific and must not be committed.
8
+ .env.local
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.5
2
+ Name: envloop-cli
3
+ Version: 0.2.1
4
+ Summary: EnvLoop CLI and Python SDK for jobs, training, checkpoints and inference
5
+ Project-URL: Homepage, https://envloop.ai
6
+ Project-URL: Documentation, https://docs.envloop.ai
7
+ Project-URL: Agent instructions, https://docs.envloop.ai/skill.md
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: tinker==0.30.2
10
+ Description-Content-Type: text/markdown
11
+
12
+ # EnvLoop CLI and Python SDK
13
+
14
+ Install the public client to run agent Jobs, inspect Trials, manage training and
15
+ checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
16
+ commands are equivalent. The Python import package is `envloop`.
17
+
18
+ ## Install
19
+
20
+ Use a Python virtual environment:
21
+
22
+ ```bash
23
+ python -m pip install envloop-cli
24
+ el --help
25
+ ```
26
+
27
+ The standard installation includes training support:
28
+
29
+ ```bash
30
+ python -c 'from envloop import Client, training; print("EnvLoop SDK ready")'
31
+ ```
32
+
33
+ The package installs the training protocol client by default. You do not need a source checkout, GitHub account,
34
+ local execution backend, Docker, or a GPU to install the client.
35
+
36
+ ## Connect
37
+
38
+ Use an authorized EnvLoop account and project. Browser authorization:
39
+
40
+ ```bash
41
+ el auth login
42
+ el auth status --check
43
+ ```
44
+
45
+ For an existing project API key, use the masked configuration wizard:
46
+
47
+ ```bash
48
+ el auth config
49
+ el auth status --check
50
+ ```
51
+
52
+ CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
53
+ `ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
54
+ your secret manager. Public API access is scoped to the authorized project.
55
+
56
+ ## Explore your project
57
+
58
+ ```bash
59
+ el job list --limit 5
60
+ el train list --limit 5
61
+ el checkpoint list --limit 5
62
+ el inference endpoint list
63
+ ```
64
+
65
+ Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
66
+ Trial commands use the spelling `trail`, for example
67
+ `el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
68
+
69
+ For the Python SDK:
70
+
71
+ ```python
72
+ from envloop import Client
73
+
74
+ with Client.remote() as client:
75
+ print(client.jobs.list(limit=5))
76
+ ```
77
+
78
+ Submitting work, training a model or starting inference can allocate billable
79
+ resources. Follow the task-specific guide and retain the outputs you need before
80
+ cleaning up temporary resources.
81
+
82
+ ## Existing installations
83
+
84
+ This distribution provides both the CLI and the `envloop` import package. Use a
85
+ fresh environment if you previously installed the separate source distribution
86
+ named `envloop`, so two distributions do not own the same Python module files.
87
+ Existing client configuration is reused; installing a package does not change
88
+ your account permissions or move remote resources. Check `el --help` when
89
+ upgrading because command groups may differ from your earlier installation.
90
+
91
+ ## Documentation
92
+
93
+ - [English documentation](https://docs.envloop.ai/)
94
+ - [中文文档](https://docs.envloop.ai/zh-cn/)
95
+ - [Installation](https://docs.envloop.ai/cli-install)
96
+ - [CLI reference](https://docs.envloop.ai/reference/cli)
97
+ - [Training and deployment](https://docs.envloop.ai/training/train-deploy)
98
+ - [Agent entry: skill.md](https://docs.envloop.ai/skill.md)
@@ -0,0 +1,87 @@
1
+ # EnvLoop CLI and Python SDK
2
+
3
+ Install the public client to run agent Jobs, inspect Trials, manage training and
4
+ checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
5
+ commands are equivalent. The Python import package is `envloop`.
6
+
7
+ ## Install
8
+
9
+ Use a Python virtual environment:
10
+
11
+ ```bash
12
+ python -m pip install envloop-cli
13
+ el --help
14
+ ```
15
+
16
+ The standard installation includes training support:
17
+
18
+ ```bash
19
+ python -c 'from envloop import Client, training; print("EnvLoop SDK ready")'
20
+ ```
21
+
22
+ The package installs the training protocol client by default. You do not need a source checkout, GitHub account,
23
+ local execution backend, Docker, or a GPU to install the client.
24
+
25
+ ## Connect
26
+
27
+ Use an authorized EnvLoop account and project. Browser authorization:
28
+
29
+ ```bash
30
+ el auth login
31
+ el auth status --check
32
+ ```
33
+
34
+ For an existing project API key, use the masked configuration wizard:
35
+
36
+ ```bash
37
+ el auth config
38
+ el auth status --check
39
+ ```
40
+
41
+ CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
42
+ `ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
43
+ your secret manager. Public API access is scoped to the authorized project.
44
+
45
+ ## Explore your project
46
+
47
+ ```bash
48
+ el job list --limit 5
49
+ el train list --limit 5
50
+ el checkpoint list --limit 5
51
+ el inference endpoint list
52
+ ```
53
+
54
+ Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
55
+ Trial commands use the spelling `trail`, for example
56
+ `el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
57
+
58
+ For the Python SDK:
59
+
60
+ ```python
61
+ from envloop import Client
62
+
63
+ with Client.remote() as client:
64
+ print(client.jobs.list(limit=5))
65
+ ```
66
+
67
+ Submitting work, training a model or starting inference can allocate billable
68
+ resources. Follow the task-specific guide and retain the outputs you need before
69
+ cleaning up temporary resources.
70
+
71
+ ## Existing installations
72
+
73
+ This distribution provides both the CLI and the `envloop` import package. Use a
74
+ fresh environment if you previously installed the separate source distribution
75
+ named `envloop`, so two distributions do not own the same Python module files.
76
+ Existing client configuration is reused; installing a package does not change
77
+ your account permissions or move remote resources. Check `el --help` when
78
+ upgrading because command groups may differ from your earlier installation.
79
+
80
+ ## Documentation
81
+
82
+ - [English documentation](https://docs.envloop.ai/)
83
+ - [中文文档](https://docs.envloop.ai/zh-cn/)
84
+ - [Installation](https://docs.envloop.ai/cli-install)
85
+ - [CLI reference](https://docs.envloop.ai/reference/cli)
86
+ - [Training and deployment](https://docs.envloop.ai/training/train-deploy)
87
+ - [Agent entry: skill.md](https://docs.envloop.ai/skill.md)
@@ -0,0 +1,5 @@
1
+ """EnvLoop Python client."""
2
+
3
+ from .client import Client
4
+
5
+ __all__ = ["Client"]
@@ -0,0 +1,208 @@
1
+ """Agent runs: a read-only view of Train Jobs grouped by their agent's run label.
2
+
3
+ The platform stores no run object. Agents tag every Train Job they create with
4
+ ``user_metadata`` (the RSI loop: ``rsi_run``, ``rsi_round``, ``rsi_checkpoint``...);
5
+ this module pages through ``GET /v1/train/jobs`` and derives runs from those tags,
6
+ using the same rules as the Console's agent runs page.
7
+ """
8
+
9
+ from datetime import datetime
10
+
11
+ from .train import TrainJobs
12
+
13
+ # The generic ``agent_*`` key wins over the RSI loop's ``rsi_*`` one (the Console's rule).
14
+ KEYS = {
15
+ "run": ("agent_run", "rsi_run"),
16
+ "round": ("agent_round", "rsi_round"),
17
+ "purpose": ("agent_purpose", "rsi_purpose"),
18
+ "method": ("agent_method", "rsi_method"),
19
+ "checkpoint": ("agent_checkpoint", "rsi_checkpoint"),
20
+ }
21
+ RUN_KEYS = KEYS["run"] # first non-empty string wins
22
+ ACTIVE_STATUSES = frozenset({"LAUNCHING", "STARTING", "RUNNING", "RECOVERING"})
23
+ TERMINAL_STATUSES = frozenset({"SUCCEEDED", "FAILED", "CANCELLED"})
24
+ PAGE_LIMIT = 100 # the platform's maximum page size
25
+
26
+
27
+ class AgentRunError(RuntimeError):
28
+ """A run lookup or listing failed; ``code`` is safe to print."""
29
+
30
+ def __init__(self, code: str) -> None:
31
+ self.code = code
32
+ super().__init__(code)
33
+
34
+
35
+ def _metadata(job) -> dict:
36
+ metadata = job.get("user_metadata")
37
+ return metadata if isinstance(metadata, dict) else {}
38
+
39
+
40
+ def _field(job, name: str):
41
+ metadata = _metadata(job)
42
+ for key in KEYS[name]:
43
+ value = metadata.get(key)
44
+ if value is not None and value != "":
45
+ return value
46
+ return None
47
+
48
+
49
+ def run_key(job) -> str | None:
50
+ """The run a Train Job belongs to, or None when it carries no run label."""
51
+ metadata = _metadata(job)
52
+ for key in RUN_KEYS:
53
+ value = metadata.get(key)
54
+ if isinstance(value, str) and value.strip():
55
+ return value
56
+ return None
57
+
58
+
59
+ def _round(job) -> int | None:
60
+ value = _field(job, "round")
61
+ if isinstance(value, int) and not isinstance(value, bool):
62
+ return value
63
+ if isinstance(value, str):
64
+ try:
65
+ return int(value.strip())
66
+ except ValueError:
67
+ return None
68
+ return None
69
+
70
+
71
+ def _order(job):
72
+ # Creation order, as the platform pages jobs (newest first, reversed here).
73
+ return (str(job.get("created_at") or ""), str(job.get("train_job_id") or ""))
74
+
75
+
76
+ def _duration_sec(job) -> int | None:
77
+ if job.get("status") not in TERMINAL_STATUSES:
78
+ return None
79
+ try:
80
+ seconds = (datetime.fromisoformat(job["updated_at"])
81
+ - datetime.fromisoformat(job["created_at"])).total_seconds()
82
+ except (KeyError, TypeError, ValueError):
83
+ return None
84
+ return round(seconds) if seconds >= 0 else None
85
+
86
+
87
+ def _checkpoint(job) -> dict | None:
88
+ """The checkpoint a job produced: its agent's version name and platform path."""
89
+ stored = job.get("checkpoint")
90
+ path = stored.get("path") if isinstance(stored, dict) else None
91
+ name = _field(job, "checkpoint")
92
+ if not isinstance(path, str) or not path:
93
+ return None
94
+ return {"name": name, "path": path}
95
+
96
+
97
+ def _job_view(job) -> dict:
98
+ metadata = _metadata(job)
99
+ return {
100
+ "train_job_id": job.get("train_job_id"),
101
+ "status": job.get("status"),
102
+ "round": _round(job),
103
+ "purpose": _field(job, "purpose"),
104
+ "method": _field(job, "method"),
105
+ "transitions": list(job.get("transitions") or []),
106
+ "created_at": job.get("created_at"),
107
+ "updated_at": job.get("updated_at"),
108
+ "duration_sec": _duration_sec(job),
109
+ "base_model": job.get("base_model"),
110
+ "lora_rank": job.get("lora_rank"),
111
+ "model_id": job.get("model_id"),
112
+ "checkpoint": _checkpoint(job),
113
+ "user_metadata": metadata,
114
+ }
115
+
116
+
117
+ def _summary(run: str, jobs: list) -> dict:
118
+ """Summarize one run's jobs, given in creation order."""
119
+ latest = jobs[-1]
120
+ if any(job.get("status") in ACTIVE_STATUSES for job in jobs):
121
+ status = "running"
122
+ else:
123
+ status = str(latest.get("status") or "unknown").lower()
124
+ succeeded = [job for job in jobs if job.get("status") == "SUCCEEDED"]
125
+ latest_saved = next((job for job in reversed(jobs) if _checkpoint(job) is not None), None)
126
+ latest_checkpoint = _checkpoint(latest_saved) if latest_saved is not None else None
127
+ if latest_checkpoint is not None:
128
+ latest_checkpoint["train_job_id"] = latest_saved.get("train_job_id")
129
+ rounds = [number for number in map(_round, jobs) if number is not None]
130
+ activity = [stamp for job in jobs if isinstance(stamp := job.get("updated_at") or job.get("created_at"), str)]
131
+ return {
132
+ "run": run,
133
+ "agent": _metadata(latest).get("source"),
134
+ "status": status,
135
+ "latest_round": max(rounds, default=None),
136
+ "trainings": {"succeeded": len(succeeded), "total": len(jobs)},
137
+ "latest_checkpoint": latest_checkpoint,
138
+ "base_model": latest.get("base_model"),
139
+ "created_at": jobs[0].get("created_at"),
140
+ "updated_at": max(activity, default=None),
141
+ }
142
+
143
+
144
+ def _detail(run: str, jobs: list) -> dict:
145
+ views = [_job_view(job) for job in jobs]
146
+ rounds: dict = {}
147
+ for view in views:
148
+ rounds.setdefault(view["round"], []).append(view)
149
+ ordered = sorted(rounds, key=lambda number: (number is None, number or 0))
150
+ checkpoints = [
151
+ {**view["checkpoint"], "round": view["round"], "train_job_id": view["train_job_id"],
152
+ "created_at": view["created_at"]}
153
+ for view in views if view["checkpoint"] is not None
154
+ ]
155
+ return {
156
+ **_summary(run, jobs),
157
+ "rounds": [{"round": number, "jobs": rounds[number]} for number in ordered],
158
+ "checkpoints": checkpoints,
159
+ }
160
+
161
+
162
+ class AgentRuns:
163
+ """Read-only agent runs derived from the visible Train Jobs listing."""
164
+
165
+ def __init__(self, train_jobs: TrainJobs) -> None:
166
+ self._train_jobs = train_jobs
167
+
168
+ def _runs(self):
169
+ jobs, ids, seen, cursor, observed_at = [], set(), set(), None, None
170
+ while True:
171
+ page = self._train_jobs.list(cursor=cursor, limit=PAGE_LIMIT)
172
+ if observed_at is None:
173
+ observed_at = page.get("observed_at")
174
+ for job in page.get("items") or []:
175
+ job_id = job.get("train_job_id") if isinstance(job, dict) else None
176
+ if not isinstance(job, dict) or job_id in ids:
177
+ continue
178
+ if job_id is not None:
179
+ ids.add(job_id)
180
+ jobs.append(job)
181
+ cursor = page.get("next_cursor")
182
+ if cursor is None:
183
+ break
184
+ if cursor in seen:
185
+ raise AgentRunError("TRAIN_JOB_CURSOR_REPEATED")
186
+ seen.add(cursor)
187
+ runs: dict = {}
188
+ for job in jobs:
189
+ key = run_key(job)
190
+ if key is not None:
191
+ runs.setdefault(key, []).append(job)
192
+ for group in runs.values():
193
+ group.sort(key=_order)
194
+ return runs, observed_at
195
+
196
+ def list(self) -> dict:
197
+ """Every run, most recently updated first, as ``{"items": [...], "observed_at"}``."""
198
+ runs, observed_at = self._runs()
199
+ items = sorted((_summary(run, jobs) for run, jobs in runs.items()),
200
+ key=lambda item: (item["updated_at"] or "", item["run"]), reverse=True)
201
+ return {"items": items, "observed_at": observed_at}
202
+
203
+ def get(self, run: str) -> dict:
204
+ """One run's summary, its rounds in order with their Train Jobs, and its checkpoints."""
205
+ runs, observed_at = self._runs()
206
+ if run not in runs:
207
+ raise AgentRunError("AGENT_RUN_NOT_FOUND")
208
+ return {**_detail(run, runs[run]), "observed_at": observed_at}