envloop-cli 0.1.0__tar.gz → 0.2.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 (38) hide show
  1. envloop_cli-0.2.0/.gitignore +8 -0
  2. envloop_cli-0.2.0/PKG-INFO +101 -0
  3. envloop_cli-0.2.0/README.pypi.md +89 -0
  4. envloop_cli-0.2.0/envloop/__init__.py +5 -0
  5. envloop_cli-0.2.0/envloop/agent_runs.py +208 -0
  6. envloop_cli-0.2.0/envloop/auth_cli.py +285 -0
  7. envloop_cli-0.2.0/envloop/auth_login.py +127 -0
  8. envloop_cli-0.2.0/envloop/auth_workspace.py +290 -0
  9. envloop_cli-0.2.0/envloop/cli.py +452 -0
  10. envloop_cli-0.2.0/envloop/cli_output.py +206 -0
  11. envloop_cli-0.2.0/envloop/client.py +548 -0
  12. envloop_cli-0.2.0/envloop/config.py +440 -0
  13. envloop_cli-0.2.0/envloop/http.py +274 -0
  14. envloop_cli-0.2.0/envloop/lifecycle_cli.py +53 -0
  15. envloop_cli-0.2.0/envloop/navigation_resources.py +260 -0
  16. envloop_cli-0.2.0/envloop/resource_cli.py +164 -0
  17. envloop_cli-0.2.0/envloop/resources.py +210 -0
  18. envloop_cli-0.2.0/envloop/train.py +51 -0
  19. envloop_cli-0.2.0/envloop/training/__init__.py +128 -0
  20. envloop_cli-0.2.0/envloop/training/credentials.py +56 -0
  21. envloop_cli-0.2.0/envloop/training/service_client.py +40 -0
  22. envloop_cli-0.2.0/pyproject.toml +60 -0
  23. envloop_cli-0.1.0/.gitignore +0 -5
  24. envloop_cli-0.1.0/PKG-INFO +0 -89
  25. envloop_cli-0.1.0/README.md +0 -81
  26. envloop_cli-0.1.0/pyproject.toml +0 -35
  27. envloop_cli-0.1.0/src/el_cli/__init__.py +0 -1
  28. envloop_cli-0.1.0/src/el_cli/cli.py +0 -257
  29. envloop_cli-0.1.0/src/el_cli/client.py +0 -335
  30. envloop_cli-0.1.0/src/el_cli/http.py +0 -115
  31. envloop_cli-0.1.0/src/el_cli/train.py +0 -53
  32. envloop_cli-0.1.0/tests/test_bearer.py +0 -117
  33. envloop_cli-0.1.0/tests/test_cli_help.py +0 -98
  34. envloop_cli-0.1.0/tests/test_job_submit_cli.py +0 -50
  35. envloop_cli-0.1.0/tests/test_jobs_http.py +0 -183
  36. envloop_cli-0.1.0/tests/test_train_cli.py +0 -42
  37. {envloop_cli-0.1.0/src/el_cli → envloop_cli-0.2.0/envloop}/utils/__init__.py +0 -0
  38. {envloop_cli-0.1.0/src/el_cli → envloop_cli-0.2.0/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,101 @@
1
+ Metadata-Version: 2.5
2
+ Name: envloop-cli
3
+ Version: 0.2.0
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
+ Provides-Extra: train
10
+ Requires-Dist: tinker==0.30.2; extra == 'train'
11
+ Description-Content-Type: text/markdown
12
+
13
+ # EnvLoop CLI and Python SDK
14
+
15
+ Install the public client to run agent Jobs, inspect Trials, manage training and
16
+ checkpoints, and deploy or call inference endpoints. The `el` and `envloop`
17
+ commands are equivalent. The Python import package is `envloop`.
18
+
19
+ ## Install
20
+
21
+ Use a Python virtual environment:
22
+
23
+ ```bash
24
+ python -m pip install envloop-cli
25
+ el --help
26
+ ```
27
+
28
+ For Python training workflows, install the optional training dependency:
29
+
30
+ ```bash
31
+ python -m pip install 'envloop-cli[train]'
32
+ python -c 'from envloop import Client, training; print("EnvLoop SDK ready")'
33
+ ```
34
+
35
+ The base client uses Python's standard library. The training extra installs the
36
+ training protocol client. You do not need a source checkout, GitHub account,
37
+ local execution backend, Docker, or a GPU to install the client.
38
+
39
+ ## Connect
40
+
41
+ Use an authorized EnvLoop account and project. Browser authorization:
42
+
43
+ ```bash
44
+ el auth login
45
+ el auth status --check
46
+ ```
47
+
48
+ For an existing project API key, use the masked configuration wizard:
49
+
50
+ ```bash
51
+ el auth config
52
+ el auth status --check
53
+ ```
54
+
55
+ CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
56
+ `ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
57
+ your secret manager. Public API access is scoped to the authorized project.
58
+
59
+ ## Explore your project
60
+
61
+ ```bash
62
+ el job list --limit 5
63
+ el train list --limit 5
64
+ el checkpoint list --limit 5
65
+ el inference endpoint list
66
+ ```
67
+
68
+ Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
69
+ Trial commands use the spelling `trail`, for example
70
+ `el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
71
+
72
+ For the Python SDK:
73
+
74
+ ```python
75
+ from envloop import Client
76
+
77
+ with Client.remote() as client:
78
+ print(client.jobs.list(limit=5))
79
+ ```
80
+
81
+ Submitting work, training a model or starting inference can allocate billable
82
+ resources. Follow the task-specific guide and retain the outputs you need before
83
+ cleaning up temporary resources.
84
+
85
+ ## Existing installations
86
+
87
+ This distribution provides both the CLI and the `envloop` import package. Use a
88
+ fresh environment if you previously installed the separate source distribution
89
+ named `envloop`, so two distributions do not own the same Python module files.
90
+ Existing client configuration is reused; installing a package does not change
91
+ your account permissions or move remote resources. Check `el --help` when
92
+ upgrading because command groups may differ from your earlier installation.
93
+
94
+ ## Documentation
95
+
96
+ - [English documentation](https://docs.envloop.ai/)
97
+ - [中文文档](https://docs.envloop.ai/zh-cn/)
98
+ - [Installation](https://docs.envloop.ai/cli-install)
99
+ - [CLI reference](https://docs.envloop.ai/reference/cli)
100
+ - [Training and deployment](https://docs.envloop.ai/training/train-deploy)
101
+ - [Agent entry: skill.md](https://docs.envloop.ai/skill.md)
@@ -0,0 +1,89 @@
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
+ For Python training workflows, install the optional training dependency:
17
+
18
+ ```bash
19
+ python -m pip install 'envloop-cli[train]'
20
+ python -c 'from envloop import Client, training; print("EnvLoop SDK ready")'
21
+ ```
22
+
23
+ The base client uses Python's standard library. The training extra installs the
24
+ training protocol client. You do not need a source checkout, GitHub account,
25
+ local execution backend, Docker, or a GPU to install the client.
26
+
27
+ ## Connect
28
+
29
+ Use an authorized EnvLoop account and project. Browser authorization:
30
+
31
+ ```bash
32
+ el auth login
33
+ el auth status --check
34
+ ```
35
+
36
+ For an existing project API key, use the masked configuration wizard:
37
+
38
+ ```bash
39
+ el auth config
40
+ el auth status --check
41
+ ```
42
+
43
+ CI can read credentials from `ENVLOOP_API_KEY` and connection settings from
44
+ `ENVLOOP_BASE_URL`, `ENVLOOP_WORKSPACE_ID` and `ENVLOOP_PROJECT_ID`. Keep keys in
45
+ your secret manager. Public API access is scoped to the authorized project.
46
+
47
+ ## Explore your project
48
+
49
+ ```bash
50
+ el job list --limit 5
51
+ el train list --limit 5
52
+ el checkpoint list --limit 5
53
+ el inference endpoint list
54
+ ```
55
+
56
+ Query a returned Job ID with `el job get JOB_ID` and `el job result JOB_ID`.
57
+ Trial commands use the spelling `trail`, for example
58
+ `el trail artifact list TRIAL_ID`. Use each command's `--help` for options.
59
+
60
+ For the Python SDK:
61
+
62
+ ```python
63
+ from envloop import Client
64
+
65
+ with Client.remote() as client:
66
+ print(client.jobs.list(limit=5))
67
+ ```
68
+
69
+ Submitting work, training a model or starting inference can allocate billable
70
+ resources. Follow the task-specific guide and retain the outputs you need before
71
+ cleaning up temporary resources.
72
+
73
+ ## Existing installations
74
+
75
+ This distribution provides both the CLI and the `envloop` import package. Use a
76
+ fresh environment if you previously installed the separate source distribution
77
+ named `envloop`, so two distributions do not own the same Python module files.
78
+ Existing client configuration is reused; installing a package does not change
79
+ your account permissions or move remote resources. Check `el --help` when
80
+ upgrading because command groups may differ from your earlier installation.
81
+
82
+ ## Documentation
83
+
84
+ - [English documentation](https://docs.envloop.ai/)
85
+ - [中文文档](https://docs.envloop.ai/zh-cn/)
86
+ - [Installation](https://docs.envloop.ai/cli-install)
87
+ - [CLI reference](https://docs.envloop.ai/reference/cli)
88
+ - [Training and deployment](https://docs.envloop.ai/training/train-deploy)
89
+ - [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}