lucen-cli 0.1.0__py3-none-any.whl
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.
- lucen/__init__.py +72 -0
- lucen/errors.py +141 -0
- lucen/hub.py +889 -0
- lucen/progress.py +96 -0
- lucen/py.typed +0 -0
- lucen/results.py +885 -0
- lucen_cli/__init__.py +3 -0
- lucen_cli/checkpoint_dir.py +226 -0
- lucen_cli/checkpoint_publisher.py +269 -0
- lucen_cli/client.py +1730 -0
- lucen_cli/cmd_auth.py +256 -0
- lucen_cli/cmd_datasets.py +190 -0
- lucen_cli/cmd_holds.py +76 -0
- lucen_cli/cmd_hub.py +245 -0
- lucen_cli/cmd_model.py +175 -0
- lucen_cli/cmd_org.py +154 -0
- lucen_cli/cmd_quickstart.py +320 -0
- lucen_cli/cmd_repo.py +87 -0
- lucen_cli/cmd_robot.py +784 -0
- lucen_cli/cmd_rollout.py +576 -0
- lucen_cli/cmd_runs.py +615 -0
- lucen_cli/cmd_serve.py +438 -0
- lucen_cli/cmd_worker.py +135 -0
- lucen_cli/config.py +268 -0
- lucen_cli/context.py +43 -0
- lucen_cli/contract_derive.py +491 -0
- lucen_cli/embodiment.py +522 -0
- lucen_cli/errors.py +210 -0
- lucen_cli/gate_scorer.py +168 -0
- lucen_cli/group.py +104 -0
- lucen_cli/hf_dataset.py +696 -0
- lucen_cli/live.py +189 -0
- lucen_cli/local.py +160 -0
- lucen_cli/main.py +277 -0
- lucen_cli/model_card.py +523 -0
- lucen_cli/offscreen.py +52 -0
- lucen_cli/onnx_manifest.py +121 -0
- lucen_cli/onnx_writer.py +313 -0
- lucen_cli/ort_quiet.py +75 -0
- lucen_cli/output.py +868 -0
- lucen_cli/progress.py +133 -0
- lucen_cli/pull.py +158 -0
- lucen_cli/push.py +384 -0
- lucen_cli/rl/__init__.py +8 -0
- lucen_cli/rl/ppo.py +316 -0
- lucen_cli/rl/tasks.py +298 -0
- lucen_cli/rl/vecenv.py +246 -0
- lucen_cli/robot/__init__.py +66 -0
- lucen_cli/robot/checks.py +1416 -0
- lucen_cli/robot/confine.py +176 -0
- lucen_cli/robot/draft.py +523 -0
- lucen_cli/robot/hf.py +218 -0
- lucen_cli/robot/hub.py +227 -0
- lucen_cli/robot/intake.py +590 -0
- lucen_cli/robot/model.py +334 -0
- lucen_cli/robot/pose.py +132 -0
- lucen_cli/robot/publish.py +197 -0
- lucen_cli/robot/refs.py +212 -0
- lucen_cli/robot/thumbnail.py +586 -0
- lucen_cli/robot/urdf.py +435 -0
- lucen_cli/robot/worker.py +599 -0
- lucen_cli/robot_assets.py +127 -0
- lucen_cli/rollout/__init__.py +12 -0
- lucen_cli/rollout/assets.py +135 -0
- lucen_cli/rollout/conditions.py +486 -0
- lucen_cli/rollout/contract.py +314 -0
- lucen_cli/rollout/engine.py +1808 -0
- lucen_cli/rollout/harness.py +201 -0
- lucen_cli/rollout/obs.py +262 -0
- lucen_cli/rollout/plant.py +456 -0
- lucen_cli/rollout/posture.py +96 -0
- lucen_cli/rollout/video.py +548 -0
- lucen_cli/rollout/worker.py +505 -0
- lucen_cli/serve/__init__.py +22 -0
- lucen_cli/serve/lerobot_backend.py +337 -0
- lucen_cli/serve/msgpack_numpy.py +59 -0
- lucen_cli/serve/policy.py +276 -0
- lucen_cli/serve/server.py +474 -0
- lucen_cli/serve/session.py +142 -0
- lucen_cli/serve/stub_skill.py +264 -0
- lucen_cli/serve/vla.py +788 -0
- lucen_cli/serve/worker.py +578 -0
- lucen_cli/sim_words.py +55 -0
- lucen_cli/state.py +237 -0
- lucen_cli/templates/__init__.py +94 -0
- lucen_cli/templates/ppo_isaac.py +914 -0
- lucen_cli/templates/ppo_mujoco.py +664 -0
- lucen_cli/templates/smolvla_lora.py +986 -0
- lucen_cli/templates/stub_ppo.py +597 -0
- lucen_cli/templates/stub_skill.py +180 -0
- lucen_cli/tfevents.py +323 -0
- lucen_cli/training_curve.py +70 -0
- lucen_cli/transfer.py +209 -0
- lucen_cli/vla_manifest.py +322 -0
- lucen_cli/worker.py +1433 -0
- lucen_cli-0.1.0.dist-info/METADATA +113 -0
- lucen_cli-0.1.0.dist-info/RECORD +100 -0
- lucen_cli-0.1.0.dist-info/WHEEL +4 -0
- lucen_cli-0.1.0.dist-info/entry_points.txt +3 -0
- lucen_cli-0.1.0.dist-info/licenses/LICENSE +202 -0
lucen/results.py
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
"""What a call returns: small frozen records with the fields a script wants.
|
|
2
|
+
|
|
3
|
+
Each one carries ids, URLs and counts as plain attributes, a `to_dict()` that
|
|
4
|
+
is the same object `lucen … --json` prints where the CLI has one, and `raw` —
|
|
5
|
+
the generated SDK model (or the CLI engine's own result) with every field the
|
|
6
|
+
contract defines, for the one case the record does not cover.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from collections.abc import Mapping
|
|
12
|
+
from dataclasses import asdict, dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from lucen_sdk.models import (
|
|
17
|
+
Episode as SdkEpisode,
|
|
18
|
+
)
|
|
19
|
+
from lucen_sdk.models import (
|
|
20
|
+
Me as SdkMe,
|
|
21
|
+
)
|
|
22
|
+
from lucen_sdk.models import (
|
|
23
|
+
Member as SdkMember,
|
|
24
|
+
)
|
|
25
|
+
from lucen_sdk.models import (
|
|
26
|
+
Org as SdkOrg,
|
|
27
|
+
)
|
|
28
|
+
from lucen_sdk.models import (
|
|
29
|
+
OrgMembership as SdkOrgMembership,
|
|
30
|
+
)
|
|
31
|
+
from lucen_sdk.models import (
|
|
32
|
+
Repo as SdkRepo,
|
|
33
|
+
)
|
|
34
|
+
from lucen_sdk.models import (
|
|
35
|
+
Rollout as SdkRollout,
|
|
36
|
+
)
|
|
37
|
+
from lucen_sdk.models import (
|
|
38
|
+
Run as SdkRun,
|
|
39
|
+
)
|
|
40
|
+
from lucen_sdk.models import (
|
|
41
|
+
RunEstimate as SdkRunEstimate,
|
|
42
|
+
)
|
|
43
|
+
from lucen_sdk.models import (
|
|
44
|
+
RunRecommendation as SdkRunRecommendation,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
from lucen_cli.client import SseEvent, unset_to_none
|
|
48
|
+
from lucen_cli.config import page_url
|
|
49
|
+
from lucen_cli.local import human_bytes
|
|
50
|
+
from lucen_cli.output import repo_dict, rollout_dict, run_dict
|
|
51
|
+
from lucen_cli.pull import PullResult as CliPullResult
|
|
52
|
+
from lucen_cli.push import PushResult as CliPushResult
|
|
53
|
+
from lucen_cli.sim_words import passing, suite_label
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _public(record: Any) -> dict[str, Any]:
|
|
57
|
+
"""`asdict` without `raw` (an SDK model is not JSON)."""
|
|
58
|
+
data = asdict(record)
|
|
59
|
+
data.pop("raw", None)
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Who you are
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class Me:
|
|
70
|
+
"""`GET /v1/auth/me`: the user and what the credential may do."""
|
|
71
|
+
|
|
72
|
+
username: str
|
|
73
|
+
display_name: str | None
|
|
74
|
+
#: `api_key` or `session`.
|
|
75
|
+
auth_kind: str
|
|
76
|
+
#: The nested scopes the key grants: `read` ⊂ `write` ⊂ `train`.
|
|
77
|
+
scopes: tuple[str, ...]
|
|
78
|
+
key_id: str | None
|
|
79
|
+
#: `lucen_sk_xxxxxxxx…` — never the whole key.
|
|
80
|
+
key_prefix: str
|
|
81
|
+
#: `env`, `file` or `argument`: where the key came from.
|
|
82
|
+
key_source: str
|
|
83
|
+
api_url: str
|
|
84
|
+
raw: SdkMe = field(repr=False, compare=False)
|
|
85
|
+
|
|
86
|
+
@classmethod
|
|
87
|
+
def from_sdk(cls, me: SdkMe, *, key_prefix: str, key_source: str, api_url: str) -> Me:
|
|
88
|
+
return cls(
|
|
89
|
+
username=me.user.username,
|
|
90
|
+
display_name=unset_to_none(me.user.display_name),
|
|
91
|
+
auth_kind=str(me.auth.kind),
|
|
92
|
+
scopes=tuple(str(scope) for scope in me.auth.scopes),
|
|
93
|
+
key_id=unset_to_none(me.auth.key_id),
|
|
94
|
+
key_prefix=key_prefix,
|
|
95
|
+
key_source=key_source,
|
|
96
|
+
api_url=api_url,
|
|
97
|
+
raw=me,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def can(self, scope: str) -> bool:
|
|
101
|
+
"""Whether the credential grants `scope` (`read`, `write` or `train`)."""
|
|
102
|
+
return scope in self.scopes
|
|
103
|
+
|
|
104
|
+
def to_dict(self) -> dict[str, Any]:
|
|
105
|
+
return _public(self)
|
|
106
|
+
|
|
107
|
+
def __str__(self) -> str:
|
|
108
|
+
return (
|
|
109
|
+
f"{self.username} · key {self.key_prefix} from {self.key_source} · "
|
|
110
|
+
f"scopes {', '.join(self.scopes) or 'none'} · {self.api_url}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
# Repos, push, pull
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class Repo:
|
|
121
|
+
"""A dataset, model or robot repo as the hub lists it."""
|
|
122
|
+
|
|
123
|
+
id: str
|
|
124
|
+
full_name: str
|
|
125
|
+
owner: str
|
|
126
|
+
slug: str
|
|
127
|
+
#: `dataset`, `model` or `robot`.
|
|
128
|
+
kind: str
|
|
129
|
+
#: `public` or `private`.
|
|
130
|
+
visibility: str
|
|
131
|
+
description: str | None
|
|
132
|
+
tags: tuple[str, ...]
|
|
133
|
+
#: The repo's page on the hub's web app, when the hub names one.
|
|
134
|
+
url: str | None
|
|
135
|
+
created_at: datetime
|
|
136
|
+
updated_at: datetime
|
|
137
|
+
raw: SdkRepo = field(repr=False, compare=False)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_sdk(cls, repo: SdkRepo, *, web: str | None) -> Repo:
|
|
141
|
+
return cls(
|
|
142
|
+
id=repo.id,
|
|
143
|
+
full_name=repo.full_name,
|
|
144
|
+
owner=repo.owner.handle,
|
|
145
|
+
slug=repo.slug,
|
|
146
|
+
kind=str(repo.kind),
|
|
147
|
+
visibility=str(repo.visibility),
|
|
148
|
+
description=unset_to_none(repo.description),
|
|
149
|
+
tags=tuple(repo.tags),
|
|
150
|
+
url=page_url(web, f"/{repo.full_name}"),
|
|
151
|
+
created_at=repo.created_at,
|
|
152
|
+
updated_at=repo.updated_at,
|
|
153
|
+
raw=repo,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def public(self) -> bool:
|
|
158
|
+
return self.visibility == "public"
|
|
159
|
+
|
|
160
|
+
def to_dict(self) -> dict[str, Any]:
|
|
161
|
+
"""The row `lucen repo ls --json` prints."""
|
|
162
|
+
return repo_dict(self.raw)
|
|
163
|
+
|
|
164
|
+
def __str__(self) -> str:
|
|
165
|
+
return f"{self.full_name} ({self.kind}, {self.visibility})"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class PushResult:
|
|
170
|
+
"""What :meth:`lucen.Hub.push` did, in numbers a script can assert on."""
|
|
171
|
+
|
|
172
|
+
repo: str
|
|
173
|
+
url: str | None
|
|
174
|
+
#: True when the push created the repo.
|
|
175
|
+
created: bool
|
|
176
|
+
kind: str
|
|
177
|
+
visibility: str
|
|
178
|
+
files_total: int
|
|
179
|
+
#: Files the hub already held at that path with that sha256: untouched.
|
|
180
|
+
files_unchanged: int
|
|
181
|
+
#: Files the hub already held under another path or account: linked, no bytes.
|
|
182
|
+
files_linked: int
|
|
183
|
+
files_uploaded: int
|
|
184
|
+
parts_uploaded: int
|
|
185
|
+
bytes_uploaded: int
|
|
186
|
+
bytes_total: int
|
|
187
|
+
dry_run: bool
|
|
188
|
+
#: With `dry_run=True`: the paths that would have uploaded.
|
|
189
|
+
would_upload: tuple[str, ...]
|
|
190
|
+
raw: CliPushResult = field(repr=False, compare=False)
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def from_cli(cls, result: CliPushResult) -> PushResult:
|
|
194
|
+
return cls(
|
|
195
|
+
repo=result.repo,
|
|
196
|
+
url=result.url,
|
|
197
|
+
created=result.created,
|
|
198
|
+
kind=result.kind,
|
|
199
|
+
visibility=result.visibility,
|
|
200
|
+
files_total=result.files_total,
|
|
201
|
+
files_unchanged=result.files_unchanged,
|
|
202
|
+
files_linked=result.files_linked,
|
|
203
|
+
files_uploaded=result.files_uploaded,
|
|
204
|
+
parts_uploaded=result.parts_uploaded,
|
|
205
|
+
bytes_uploaded=result.bytes_uploaded,
|
|
206
|
+
bytes_total=result.bytes_total,
|
|
207
|
+
dry_run=result.dry_run,
|
|
208
|
+
would_upload=tuple(result.would_upload),
|
|
209
|
+
raw=result,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
def to_dict(self) -> dict[str, Any]:
|
|
213
|
+
"""The object `lucen push --json` prints."""
|
|
214
|
+
return self.raw.to_dict()
|
|
215
|
+
|
|
216
|
+
def __str__(self) -> str:
|
|
217
|
+
verb = "Created and pushed" if self.created else "Pushed"
|
|
218
|
+
return (
|
|
219
|
+
f"{verb} {self.repo}: {self.files_total} file(s), {self.files_uploaded} uploaded "
|
|
220
|
+
f"({human_bytes(self.bytes_uploaded)}), {self.files_linked} deduped, "
|
|
221
|
+
f"{self.files_unchanged} unchanged"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@dataclass(frozen=True)
|
|
226
|
+
class PullResult:
|
|
227
|
+
"""What :meth:`lucen.Hub.pull` did. Every downloaded file's sha256 was verified."""
|
|
228
|
+
|
|
229
|
+
repo: str
|
|
230
|
+
#: The destination directory, absolute.
|
|
231
|
+
path: str
|
|
232
|
+
files_total: int
|
|
233
|
+
files_downloaded: int
|
|
234
|
+
#: Files already on disk with the right size and sha256.
|
|
235
|
+
files_skipped: int
|
|
236
|
+
bytes_downloaded: int
|
|
237
|
+
bytes_total: int
|
|
238
|
+
raw: CliPullResult = field(repr=False, compare=False)
|
|
239
|
+
|
|
240
|
+
@classmethod
|
|
241
|
+
def from_cli(cls, result: CliPullResult) -> PullResult:
|
|
242
|
+
return cls(
|
|
243
|
+
repo=result.repo,
|
|
244
|
+
path=result.dest,
|
|
245
|
+
files_total=result.files_total,
|
|
246
|
+
files_downloaded=result.files_downloaded,
|
|
247
|
+
files_skipped=result.files_skipped,
|
|
248
|
+
bytes_downloaded=result.bytes_downloaded,
|
|
249
|
+
bytes_total=result.bytes_total,
|
|
250
|
+
raw=result,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def to_dict(self) -> dict[str, Any]:
|
|
254
|
+
"""The object `lucen pull --json` prints."""
|
|
255
|
+
return self.raw.to_dict()
|
|
256
|
+
|
|
257
|
+
def __str__(self) -> str:
|
|
258
|
+
return (
|
|
259
|
+
f"Pulled {self.repo}: {self.files_total} file(s), {self.files_downloaded} downloaded "
|
|
260
|
+
f"({human_bytes(self.bytes_downloaded)}), {self.files_skipped} already local"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
# Runs
|
|
266
|
+
# ---------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass(frozen=True)
|
|
270
|
+
class Gate:
|
|
271
|
+
"""A run's sim-gate count: `passed` of `total` cells PASS on its latest scorecard."""
|
|
272
|
+
|
|
273
|
+
passed: int
|
|
274
|
+
total: int
|
|
275
|
+
|
|
276
|
+
def __str__(self) -> str:
|
|
277
|
+
return f"{self.passed} / {self.total} PASS"
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@dataclass(frozen=True)
|
|
281
|
+
class Run:
|
|
282
|
+
"""A training run as the hub reads it back."""
|
|
283
|
+
|
|
284
|
+
id: str
|
|
285
|
+
#: `queued`, `running`, `succeeded`, `failed` or `canceled`.
|
|
286
|
+
status: str
|
|
287
|
+
#: `cloud` (executed), `external` (imported) or `orchestration`.
|
|
288
|
+
kind: str
|
|
289
|
+
template: str
|
|
290
|
+
#: `worker` (a runner you start) or `modal` (the hub's GPU backend).
|
|
291
|
+
executor: str | None
|
|
292
|
+
owner: str
|
|
293
|
+
parent_run_id: str | None
|
|
294
|
+
claimed_by: str | None
|
|
295
|
+
#: `owner/slug` of the model repo the run published, once it has.
|
|
296
|
+
output_model_repo: str | None
|
|
297
|
+
#: The last metrics line the runner reported.
|
|
298
|
+
latest_metrics: dict[str, Any] | None
|
|
299
|
+
#: The sim gate's count on the run's scorecard, when it has one.
|
|
300
|
+
gate: Gate | None
|
|
301
|
+
cost_usd: float | None
|
|
302
|
+
error: str | None
|
|
303
|
+
#: The run's page on the hub's web app, when the hub names one.
|
|
304
|
+
url: str | None
|
|
305
|
+
created_at: datetime
|
|
306
|
+
finished_at: datetime | None
|
|
307
|
+
#: On a create: whether this call made the run (False on an idempotent replay).
|
|
308
|
+
created: bool | None
|
|
309
|
+
raw: SdkRun = field(repr=False, compare=False)
|
|
310
|
+
|
|
311
|
+
@classmethod
|
|
312
|
+
def from_sdk(cls, run: SdkRun, *, web: str | None, created: bool | None = None) -> Run:
|
|
313
|
+
data = run_dict(run)
|
|
314
|
+
gate = unset_to_none(run.gate)
|
|
315
|
+
return cls(
|
|
316
|
+
id=run.id,
|
|
317
|
+
status=data["status"],
|
|
318
|
+
kind=data["kind"],
|
|
319
|
+
template=data["template"],
|
|
320
|
+
executor=data["executor"],
|
|
321
|
+
owner=data["owner"],
|
|
322
|
+
parent_run_id=data["parent_run_id"],
|
|
323
|
+
claimed_by=data["claimed_by"],
|
|
324
|
+
output_model_repo=data["output_model_repo"],
|
|
325
|
+
latest_metrics=data["latest_metrics"],
|
|
326
|
+
gate=Gate(gate.passed, gate.total) if gate is not None else None,
|
|
327
|
+
cost_usd=data["cost_usd"],
|
|
328
|
+
error=data["error"],
|
|
329
|
+
url=page_url(web, f"/runs/{run.id}"),
|
|
330
|
+
created_at=run.created_at,
|
|
331
|
+
finished_at=unset_to_none(run.finished_at),
|
|
332
|
+
created=created,
|
|
333
|
+
raw=run,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@property
|
|
337
|
+
def done(self) -> bool:
|
|
338
|
+
"""True once the run is `succeeded`, `failed` or `canceled`."""
|
|
339
|
+
return self.status in ("succeeded", "failed", "canceled")
|
|
340
|
+
|
|
341
|
+
def to_dict(self) -> dict[str, Any]:
|
|
342
|
+
"""The object `lucen runs show --json` prints."""
|
|
343
|
+
data = run_dict(self.raw)
|
|
344
|
+
data["url"] = self.url
|
|
345
|
+
if self.created is not None:
|
|
346
|
+
data["created"] = self.created
|
|
347
|
+
return data
|
|
348
|
+
|
|
349
|
+
def __str__(self) -> str:
|
|
350
|
+
bits = [f"{self.id} ({self.status}, {self.template}"]
|
|
351
|
+
if self.executor:
|
|
352
|
+
bits[0] += f" on {self.executor}"
|
|
353
|
+
bits[0] += ")"
|
|
354
|
+
if self.output_model_repo:
|
|
355
|
+
bits.append(f"model {self.output_model_repo}")
|
|
356
|
+
if self.gate is not None:
|
|
357
|
+
bits.append(f"gate {self.gate}")
|
|
358
|
+
if self.error:
|
|
359
|
+
bits.append(f"error: {self.error.splitlines()[0]}")
|
|
360
|
+
return " · ".join(bits)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclass(frozen=True)
|
|
364
|
+
class LogEvent:
|
|
365
|
+
"""One frame of a run's log stream.
|
|
366
|
+
|
|
367
|
+
`kind` is ``log`` (a line: `line`, with `stream` ``stdout`` for the
|
|
368
|
+
trainer's own output and ``stderr`` for a notice the hub wrote into the
|
|
369
|
+
log — a cancel request, a re-queue), ``metric`` (one `metrics.jsonl` line
|
|
370
|
+
as `metrics`, numbered by `index`), or ``end`` (the stream closed; `status`
|
|
371
|
+
is the run's status at that moment).
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
kind: str
|
|
375
|
+
line: str | None = None
|
|
376
|
+
stream: str | None = None
|
|
377
|
+
ts: str | None = None
|
|
378
|
+
metrics: dict[str, Any] | None = None
|
|
379
|
+
index: int | None = None
|
|
380
|
+
status: str | None = None
|
|
381
|
+
raw: dict[str, Any] = field(default_factory=dict, repr=False, compare=False)
|
|
382
|
+
|
|
383
|
+
@classmethod
|
|
384
|
+
def from_sse(cls, event: SseEvent) -> LogEvent:
|
|
385
|
+
data = event.data
|
|
386
|
+
metrics = data.get("metrics")
|
|
387
|
+
index = data.get("index")
|
|
388
|
+
return cls(
|
|
389
|
+
kind=event.event,
|
|
390
|
+
line=data.get("line") if event.event == "log" else None,
|
|
391
|
+
stream=data.get("stream") if event.event == "log" else None,
|
|
392
|
+
ts=data.get("ts"),
|
|
393
|
+
metrics=dict(metrics) if isinstance(metrics, Mapping) else None,
|
|
394
|
+
index=int(index) if isinstance(index, int) else None,
|
|
395
|
+
status=data.get("status") if event.event == "end" else None,
|
|
396
|
+
raw=dict(data),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
@property
|
|
400
|
+
def is_notice(self) -> bool:
|
|
401
|
+
"""True for a line the hub wrote, not the trainer (`stream == "stderr"`)."""
|
|
402
|
+
return self.kind == "log" and self.stream == "stderr"
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def text(self) -> str:
|
|
406
|
+
"""The line as `lucen train --follow` prints it: ` | …` the runner, ` ! …` the hub."""
|
|
407
|
+
if self.kind == "log":
|
|
408
|
+
return f" {'!' if self.is_notice else '|'} {self.line or ''}"
|
|
409
|
+
if self.kind == "end":
|
|
410
|
+
return f"-- {self.status}"
|
|
411
|
+
return f" = {self.metrics}"
|
|
412
|
+
|
|
413
|
+
def __str__(self) -> str:
|
|
414
|
+
return self.text
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@dataclass(frozen=True)
|
|
418
|
+
class RunEstimate:
|
|
419
|
+
"""`POST /v1/runs/estimate`: the price of a recipe before anything is created."""
|
|
420
|
+
|
|
421
|
+
executor: str
|
|
422
|
+
tier: str | None
|
|
423
|
+
rate_usd_per_hour: float
|
|
424
|
+
#: C1: the Isaac host's share, on top of the GPU rate (0 elsewhere).
|
|
425
|
+
host_rate_usd_per_hour: float
|
|
426
|
+
max_hours: float
|
|
427
|
+
cost_usd_max: float
|
|
428
|
+
#: False on a `worker` run: your own machine, nothing billed.
|
|
429
|
+
priced: bool
|
|
430
|
+
budget_usd: float | None
|
|
431
|
+
capped_by_budget: bool
|
|
432
|
+
raw: SdkRunEstimate = field(repr=False, compare=False)
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def from_sdk(cls, estimate: SdkRunEstimate) -> RunEstimate:
|
|
436
|
+
return cls(
|
|
437
|
+
executor=str(estimate.executor),
|
|
438
|
+
tier=estimate.tier,
|
|
439
|
+
rate_usd_per_hour=estimate.rate_usd_per_hour,
|
|
440
|
+
host_rate_usd_per_hour=float(unset_to_none(estimate.host_rate_usd_per_hour) or 0.0),
|
|
441
|
+
max_hours=estimate.max_hours,
|
|
442
|
+
cost_usd_max=estimate.cost_usd_max,
|
|
443
|
+
priced=estimate.priced,
|
|
444
|
+
budget_usd=unset_to_none(estimate.budget_usd),
|
|
445
|
+
capped_by_budget=bool(unset_to_none(estimate.capped_by_budget) or False),
|
|
446
|
+
raw=estimate,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
def to_dict(self) -> dict[str, Any]:
|
|
450
|
+
return _public(self)
|
|
451
|
+
|
|
452
|
+
def __str__(self) -> str:
|
|
453
|
+
if not self.priced:
|
|
454
|
+
return f"{self.executor}: not priced (your own machine), up to {self.max_hours:g} h"
|
|
455
|
+
return (
|
|
456
|
+
f"{self.executor} on {self.tier}: ${self.rate_usd_per_hour:.4f}/h x "
|
|
457
|
+
f"{self.max_hours:g} h = at most ${self.cost_usd_max:.4f}"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
@dataclass(frozen=True)
|
|
462
|
+
class RunRecommendation:
|
|
463
|
+
"""`POST /v1/runs/recommend`: the hub's deterministic hardware planner, with its arithmetic."""
|
|
464
|
+
|
|
465
|
+
template: str
|
|
466
|
+
#: The smallest GPU tier that fits (`l4`, `a10g`, `a100`, `h100`, `l40s`), or None.
|
|
467
|
+
tier: str | None
|
|
468
|
+
vram_estimate_gb: float
|
|
469
|
+
vram_required_gb: float
|
|
470
|
+
fits_on: tuple[str, ...]
|
|
471
|
+
est_hours: float | None
|
|
472
|
+
cost_usd_est: float | None
|
|
473
|
+
cost_usd_max: float
|
|
474
|
+
priced: bool
|
|
475
|
+
#: `modelled` or `measured` (from succeeded runs of the same template).
|
|
476
|
+
basis: str
|
|
477
|
+
#: Whether the hub can execute the template, or only plan it.
|
|
478
|
+
implemented: bool
|
|
479
|
+
#: One line per step of the arithmetic.
|
|
480
|
+
why: tuple[str, ...]
|
|
481
|
+
warnings: tuple[str, ...]
|
|
482
|
+
raw: SdkRunRecommendation = field(repr=False, compare=False)
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def from_sdk(cls, rec: SdkRunRecommendation) -> RunRecommendation:
|
|
486
|
+
return cls(
|
|
487
|
+
template=rec.template,
|
|
488
|
+
tier=rec.tier,
|
|
489
|
+
vram_estimate_gb=rec.vram_estimate_gb,
|
|
490
|
+
vram_required_gb=rec.vram_required_gb,
|
|
491
|
+
fits_on=tuple(rec.fits_on),
|
|
492
|
+
est_hours=rec.est_hours,
|
|
493
|
+
cost_usd_est=rec.cost_usd_est,
|
|
494
|
+
cost_usd_max=rec.cost_usd_max,
|
|
495
|
+
priced=rec.priced,
|
|
496
|
+
basis=str(rec.basis),
|
|
497
|
+
implemented=rec.implemented,
|
|
498
|
+
why=tuple(rec.why),
|
|
499
|
+
warnings=tuple(rec.warnings),
|
|
500
|
+
raw=rec,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def to_dict(self) -> dict[str, Any]:
|
|
504
|
+
return _public(self)
|
|
505
|
+
|
|
506
|
+
def __str__(self) -> str:
|
|
507
|
+
tier = self.tier or "no tier fits"
|
|
508
|
+
return (
|
|
509
|
+
f"{self.template}: {tier} — {self.vram_required_gb:g} GB required "
|
|
510
|
+
f"({self.vram_estimate_gb:g} GB {self.basis}), fits on {', '.join(self.fits_on) or '—'}"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
# ---------------------------------------------------------------------------
|
|
515
|
+
# Rollouts (the sim gate)
|
|
516
|
+
# ---------------------------------------------------------------------------
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
@dataclass(frozen=True)
|
|
520
|
+
class Cell:
|
|
521
|
+
"""One cell of a rollout's battery, with the hub's verdict."""
|
|
522
|
+
|
|
523
|
+
name: str
|
|
524
|
+
#: `PASS` or `FAIL`, or None while the rollout is not finished.
|
|
525
|
+
verdict: str | None
|
|
526
|
+
seeds: int
|
|
527
|
+
#: Seeds that stayed upright for the whole horizon.
|
|
528
|
+
alive: int
|
|
529
|
+
#: Per-axis tracking ratio (`vx`, `vy`, `yaw`), when the cell commands one.
|
|
530
|
+
tracking: dict[str, Any]
|
|
531
|
+
detail: str | None
|
|
532
|
+
#: `velocity`, `joint_target`, `reach`, `recovery`.
|
|
533
|
+
kind: str
|
|
534
|
+
#: Presigned URLs (an hour) for the mp4 and the trajectory JSON.
|
|
535
|
+
video_url: str | None
|
|
536
|
+
traj_url: str | None
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def passed(self) -> bool:
|
|
540
|
+
return self.verdict == "PASS"
|
|
541
|
+
|
|
542
|
+
def __str__(self) -> str:
|
|
543
|
+
return f"{self.name}: {self.verdict or '—'} ({self.alive}/{self.seeds} upright)"
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
@dataclass(frozen=True)
|
|
547
|
+
class Rollout:
|
|
548
|
+
"""A rollout — a policy run in MuJoCo on a robot's own model, scored by the hub."""
|
|
549
|
+
|
|
550
|
+
id: str
|
|
551
|
+
#: `queued`, `running`, `succeeded`, `failed` or `canceled`.
|
|
552
|
+
status: str
|
|
553
|
+
#: `owner/slug` of the model repo.
|
|
554
|
+
model: str
|
|
555
|
+
#: `owner/slug` of the robot repo.
|
|
556
|
+
robot: str
|
|
557
|
+
battery: str | None
|
|
558
|
+
seed: int
|
|
559
|
+
horizon_s: float
|
|
560
|
+
#: The training run the scorecard is attached to, when one was named.
|
|
561
|
+
run_id: str | None
|
|
562
|
+
claimed_by: str | None
|
|
563
|
+
cells: tuple[Cell, ...]
|
|
564
|
+
#: Cells that PASS, out of all cells, once scored.
|
|
565
|
+
passed: int | None
|
|
566
|
+
total: int | None
|
|
567
|
+
warnings: tuple[str, ...]
|
|
568
|
+
cost_usd: float | None
|
|
569
|
+
priced: bool
|
|
570
|
+
error: str | None
|
|
571
|
+
url: str | None
|
|
572
|
+
created_at: datetime
|
|
573
|
+
finished_at: datetime | None
|
|
574
|
+
raw: SdkRollout = field(repr=False, compare=False)
|
|
575
|
+
|
|
576
|
+
@classmethod
|
|
577
|
+
def from_sdk(cls, rollout: SdkRollout, *, web: str | None) -> Rollout:
|
|
578
|
+
data = rollout_dict(rollout)
|
|
579
|
+
cells = tuple(
|
|
580
|
+
Cell(
|
|
581
|
+
name=cell["name"],
|
|
582
|
+
verdict=cell["status"],
|
|
583
|
+
seeds=cell["seeds"],
|
|
584
|
+
alive=cell["alive"],
|
|
585
|
+
tracking=dict(cell.get("tracking") or {}),
|
|
586
|
+
detail=cell.get("detail"),
|
|
587
|
+
kind=str(cell.get("kind") or "velocity"),
|
|
588
|
+
video_url=cell.get("video_url"),
|
|
589
|
+
traj_url=cell.get("traj_url"),
|
|
590
|
+
)
|
|
591
|
+
for cell in data["cells"]
|
|
592
|
+
)
|
|
593
|
+
return cls(
|
|
594
|
+
id=rollout.id,
|
|
595
|
+
status=data["status"],
|
|
596
|
+
model=data["model_repo"],
|
|
597
|
+
robot=data["robot_repo"],
|
|
598
|
+
battery=data["battery"],
|
|
599
|
+
seed=data["seed"],
|
|
600
|
+
horizon_s=data["horizon_s"],
|
|
601
|
+
run_id=data["run_id"],
|
|
602
|
+
claimed_by=data["claimed_by"],
|
|
603
|
+
cells=cells,
|
|
604
|
+
passed=data["gates_passed"],
|
|
605
|
+
total=data["gates_total"],
|
|
606
|
+
warnings=tuple(data["warnings"]),
|
|
607
|
+
cost_usd=data["cost_usd"],
|
|
608
|
+
priced=data["priced"],
|
|
609
|
+
error=data["error"],
|
|
610
|
+
url=page_url(web, f"/sim-tests/{rollout.id}"),
|
|
611
|
+
created_at=rollout.created_at,
|
|
612
|
+
finished_at=unset_to_none(rollout.finished_at),
|
|
613
|
+
raw=rollout,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
@property
|
|
617
|
+
def done(self) -> bool:
|
|
618
|
+
return self.status in ("succeeded", "failed", "canceled")
|
|
619
|
+
|
|
620
|
+
@property
|
|
621
|
+
def scorecard(self) -> dict[str, str | None]:
|
|
622
|
+
"""`{cell name: verdict}` — the hub's PASS/FAIL per cell."""
|
|
623
|
+
return {cell.name: cell.verdict for cell in self.cells}
|
|
624
|
+
|
|
625
|
+
def to_dict(self) -> dict[str, Any]:
|
|
626
|
+
"""The object `lucen rollout show --json` prints."""
|
|
627
|
+
data = rollout_dict(self.raw)
|
|
628
|
+
data["url"] = self.url
|
|
629
|
+
return data
|
|
630
|
+
|
|
631
|
+
def __str__(self) -> str:
|
|
632
|
+
# Q9: the web's words; `to_dict()` and `scorecard()` keep PASS / FAIL and `battery`.
|
|
633
|
+
head = f"Sim test {self.id} ({self.status}): {self.model} on {self.robot}, "
|
|
634
|
+
head += suite_label(self.battery)
|
|
635
|
+
if self.total is not None:
|
|
636
|
+
head += f". {passing(self.passed, self.total)}"
|
|
637
|
+
return head + "."
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ---------------------------------------------------------------------------
|
|
641
|
+
# Organizations, datasets, robots
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
@dataclass(frozen=True)
|
|
646
|
+
class Org:
|
|
647
|
+
"""An organization: a handle a lab shares."""
|
|
648
|
+
|
|
649
|
+
slug: str
|
|
650
|
+
display_name: str | None
|
|
651
|
+
description: str | None
|
|
652
|
+
#: Your role in it (`owner` or `member`), when the call knows it.
|
|
653
|
+
role: str | None
|
|
654
|
+
url: str | None
|
|
655
|
+
raw: SdkOrg = field(repr=False, compare=False)
|
|
656
|
+
|
|
657
|
+
@classmethod
|
|
658
|
+
def from_sdk(cls, org: SdkOrg, *, web: str | None, role: str | None = None) -> Org:
|
|
659
|
+
return cls(
|
|
660
|
+
slug=org.slug,
|
|
661
|
+
display_name=unset_to_none(org.display_name),
|
|
662
|
+
description=unset_to_none(org.description),
|
|
663
|
+
role=role,
|
|
664
|
+
url=page_url(web, f"/{org.slug}"),
|
|
665
|
+
raw=org,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
@classmethod
|
|
669
|
+
def from_membership(cls, membership: SdkOrgMembership, *, web: str | None) -> Org:
|
|
670
|
+
return cls.from_sdk(membership.org, web=web, role=str(membership.role))
|
|
671
|
+
|
|
672
|
+
def to_dict(self) -> dict[str, Any]:
|
|
673
|
+
return _public(self)
|
|
674
|
+
|
|
675
|
+
def __str__(self) -> str:
|
|
676
|
+
return f"{self.slug}" + (f" ({self.role})" if self.role else "")
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
@dataclass(frozen=True)
|
|
680
|
+
class Member:
|
|
681
|
+
"""A person in an organization, by hub handle."""
|
|
682
|
+
|
|
683
|
+
username: str
|
|
684
|
+
display_name: str | None
|
|
685
|
+
#: `owner` or `member`.
|
|
686
|
+
role: str
|
|
687
|
+
added_at: datetime
|
|
688
|
+
raw: SdkMember = field(repr=False, compare=False)
|
|
689
|
+
|
|
690
|
+
@classmethod
|
|
691
|
+
def from_sdk(cls, member: SdkMember) -> Member:
|
|
692
|
+
return cls(
|
|
693
|
+
username=member.user.username,
|
|
694
|
+
display_name=unset_to_none(member.user.display_name),
|
|
695
|
+
role=str(member.role),
|
|
696
|
+
added_at=member.created_at,
|
|
697
|
+
raw=member,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
def to_dict(self) -> dict[str, Any]:
|
|
701
|
+
return _public(self)
|
|
702
|
+
|
|
703
|
+
def __str__(self) -> str:
|
|
704
|
+
return f"{self.username} ({self.role})"
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@dataclass(frozen=True)
|
|
708
|
+
class Episode:
|
|
709
|
+
"""One episode of a LeRobot-format dataset, as the hub indexes it."""
|
|
710
|
+
|
|
711
|
+
index: int
|
|
712
|
+
num_frames: int
|
|
713
|
+
tasks: tuple[str, ...]
|
|
714
|
+
raw: SdkEpisode = field(repr=False, compare=False)
|
|
715
|
+
|
|
716
|
+
@classmethod
|
|
717
|
+
def from_sdk(cls, episode: SdkEpisode) -> Episode:
|
|
718
|
+
return cls(
|
|
719
|
+
index=episode.index,
|
|
720
|
+
num_frames=episode.num_frames,
|
|
721
|
+
tasks=tuple(unset_to_none(episode.tasks) or ()),
|
|
722
|
+
raw=episode,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def to_dict(self) -> dict[str, Any]:
|
|
726
|
+
return _public(self)
|
|
727
|
+
|
|
728
|
+
def __str__(self) -> str:
|
|
729
|
+
task = f" — {self.tasks[0]}" if self.tasks else ""
|
|
730
|
+
return f"episode {self.index}: {self.num_frames} frames{task}"
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
@dataclass(frozen=True)
|
|
734
|
+
class RobotDraft:
|
|
735
|
+
"""What `lucen robot init` wrote: the card file and what you still have to declare."""
|
|
736
|
+
|
|
737
|
+
#: Absolute path of the drafted `robot_card.json`.
|
|
738
|
+
card_file: str
|
|
739
|
+
directory: str
|
|
740
|
+
mjcf_path: str
|
|
741
|
+
#: `mjcf` or `urdf` (converted).
|
|
742
|
+
source: str
|
|
743
|
+
joints: tuple[str, ...]
|
|
744
|
+
#: `fixed` or `floating`.
|
|
745
|
+
base: str
|
|
746
|
+
#: What the model file could not state: one entry per field, with a severity.
|
|
747
|
+
needs_you: tuple[dict[str, Any], ...]
|
|
748
|
+
#: The whole result, as `lucen robot init --json` prints it.
|
|
749
|
+
raw: dict[str, Any] = field(repr=False, compare=False)
|
|
750
|
+
|
|
751
|
+
@classmethod
|
|
752
|
+
def from_cli(cls, result: dict[str, Any]) -> RobotDraft:
|
|
753
|
+
model = result.get("model") or {}
|
|
754
|
+
return cls(
|
|
755
|
+
card_file=str(result["card_file"]),
|
|
756
|
+
directory=str(result["directory"]),
|
|
757
|
+
mjcf_path=str(result["mjcf_path"]),
|
|
758
|
+
source=str(result["source"]),
|
|
759
|
+
joints=tuple(str(name) for name in model.get("joints") or ()),
|
|
760
|
+
base=str(model.get("base") or ""),
|
|
761
|
+
needs_you=tuple(dict(item) for item in result.get("needs_you") or ()),
|
|
762
|
+
raw=result,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
@property
|
|
766
|
+
def blocking(self) -> tuple[dict[str, Any], ...]:
|
|
767
|
+
"""The `needs_you` entries that fail validation until declared."""
|
|
768
|
+
return tuple(item for item in self.needs_you if item.get("severity") == "blocking")
|
|
769
|
+
|
|
770
|
+
def to_dict(self) -> dict[str, Any]:
|
|
771
|
+
return dict(self.raw)
|
|
772
|
+
|
|
773
|
+
def __str__(self) -> str:
|
|
774
|
+
return (
|
|
775
|
+
f"{self.card_file}: {len(self.joints)} joints, {self.base} base, "
|
|
776
|
+
f"{len(self.needs_you)} thing(s) to declare ({len(self.blocking)} blocking)"
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@dataclass(frozen=True)
|
|
781
|
+
class RobotReport:
|
|
782
|
+
"""`lucen robot validate`'s report: every check with its code, status and message."""
|
|
783
|
+
|
|
784
|
+
ok: bool
|
|
785
|
+
#: `pass`, `warn` or `fail`.
|
|
786
|
+
grade: str
|
|
787
|
+
#: `mujoco` (the CLI compiled it) or `hub` (the hub's static checks).
|
|
788
|
+
engine: str
|
|
789
|
+
checks: tuple[dict[str, Any], ...]
|
|
790
|
+
counts: dict[str, int]
|
|
791
|
+
interface_fingerprint: str | None
|
|
792
|
+
#: The whole report, as `lucen robot validate --json` prints it.
|
|
793
|
+
raw: dict[str, Any] = field(repr=False, compare=False)
|
|
794
|
+
|
|
795
|
+
@classmethod
|
|
796
|
+
def from_cli(cls, report: dict[str, Any]) -> RobotReport:
|
|
797
|
+
return cls(
|
|
798
|
+
ok=bool(report.get("ok")),
|
|
799
|
+
grade=str(report.get("grade") or ("pass" if report.get("ok") else "fail")),
|
|
800
|
+
engine=str(report.get("engine") or "mujoco"),
|
|
801
|
+
checks=tuple(dict(check) for check in report.get("checks") or ()),
|
|
802
|
+
counts=dict(report.get("counts") or {}),
|
|
803
|
+
interface_fingerprint=report.get("interface_fingerprint"),
|
|
804
|
+
raw=report,
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
def failed(self) -> tuple[dict[str, Any], ...]:
|
|
808
|
+
return tuple(check for check in self.checks if check.get("status") == "fail")
|
|
809
|
+
|
|
810
|
+
def warnings(self) -> tuple[dict[str, Any], ...]:
|
|
811
|
+
return tuple(check for check in self.checks if check.get("status") == "warn")
|
|
812
|
+
|
|
813
|
+
def to_dict(self) -> dict[str, Any]:
|
|
814
|
+
return dict(self.raw)
|
|
815
|
+
|
|
816
|
+
def __str__(self) -> str:
|
|
817
|
+
counts = self.counts or {}
|
|
818
|
+
return (
|
|
819
|
+
f"{self.grade}: {counts.get('pass', 0)} pass · {counts.get('warn', 0)} warn · "
|
|
820
|
+
f"{counts.get('fail', 0)} fail"
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
@dataclass(frozen=True)
|
|
825
|
+
class RobotPublishResult:
|
|
826
|
+
"""What `lucen robot publish` did: the repo, the push and the card the hub accepted."""
|
|
827
|
+
|
|
828
|
+
repo: str
|
|
829
|
+
url: str | None
|
|
830
|
+
created: bool
|
|
831
|
+
files_total: int
|
|
832
|
+
files_uploaded: int
|
|
833
|
+
bytes_uploaded: int
|
|
834
|
+
interface_fingerprint: str | None
|
|
835
|
+
#: The grade the CLI's own validation gave, and the hub's static one.
|
|
836
|
+
local_grade: str
|
|
837
|
+
hub_grade: str
|
|
838
|
+
raw: dict[str, Any] = field(repr=False, compare=False)
|
|
839
|
+
|
|
840
|
+
@classmethod
|
|
841
|
+
def from_cli(cls, result: dict[str, Any]) -> RobotPublishResult:
|
|
842
|
+
return cls(
|
|
843
|
+
repo=str(result["repo"]),
|
|
844
|
+
url=result.get("url"),
|
|
845
|
+
created=bool(result.get("created")),
|
|
846
|
+
files_total=int(result.get("files_total") or 0),
|
|
847
|
+
files_uploaded=int(result.get("files_uploaded") or 0),
|
|
848
|
+
bytes_uploaded=int(result.get("bytes_uploaded") or 0),
|
|
849
|
+
interface_fingerprint=result.get("interface_fingerprint"),
|
|
850
|
+
local_grade=str(result.get("local_grade") or ""),
|
|
851
|
+
hub_grade=str(result.get("hub_grade") or ""),
|
|
852
|
+
raw=result,
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
def to_dict(self) -> dict[str, Any]:
|
|
856
|
+
return dict(self.raw)
|
|
857
|
+
|
|
858
|
+
def __str__(self) -> str:
|
|
859
|
+
verb = "Created and published" if self.created else "Published"
|
|
860
|
+
return (
|
|
861
|
+
f"{verb} {self.repo}: {self.files_total} file(s), {self.files_uploaded} uploaded "
|
|
862
|
+
f"({human_bytes(self.bytes_uploaded)}); card set (local {self.local_grade}, "
|
|
863
|
+
f"hub {self.hub_grade})"
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
__all__ = [
|
|
868
|
+
"Cell",
|
|
869
|
+
"Episode",
|
|
870
|
+
"Gate",
|
|
871
|
+
"LogEvent",
|
|
872
|
+
"Me",
|
|
873
|
+
"Member",
|
|
874
|
+
"Org",
|
|
875
|
+
"PullResult",
|
|
876
|
+
"PushResult",
|
|
877
|
+
"Repo",
|
|
878
|
+
"RobotDraft",
|
|
879
|
+
"RobotPublishResult",
|
|
880
|
+
"RobotReport",
|
|
881
|
+
"Rollout",
|
|
882
|
+
"Run",
|
|
883
|
+
"RunEstimate",
|
|
884
|
+
"RunRecommendation",
|
|
885
|
+
]
|