simulo 0.12.0__tar.gz → 0.13.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.
- {simulo-0.12.0/src/simulo.egg-info → simulo-0.13.0}/PKG-INFO +22 -5
- {simulo-0.12.0 → simulo-0.13.0}/PYPI.md +17 -3
- {simulo-0.12.0 → simulo-0.13.0}/pyproject.toml +26 -2
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/app.py +184 -17
- simulo-0.13.0/src/simulo/_client/asset.py +328 -0
- simulo-0.13.0/src/simulo/_client/asset_api.py +591 -0
- simulo-0.13.0/src/simulo/_client/asset_package.py +1086 -0
- simulo-0.13.0/src/simulo/_client/asset_pins.py +187 -0
- simulo-0.13.0/src/simulo/_client/builtin_aliases.py +107 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/bundle.py +37 -13
- simulo-0.13.0/src/simulo/_client/cli.py +5011 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/config.py +74 -24
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/http.py +90 -4
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/jobs_api.py +20 -4
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/learning.py +28 -2
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/login.py +28 -5
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/manifest.py +3 -8
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/packaging.py +1 -1
- simulo-0.13.0/src/simulo/_client/runtime.py +90 -0
- simulo-0.13.0/src/simulo/_client/runtime_display.py +90 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/submit_api.py +110 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/inference/app.py.tmpl +14 -20
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/scenario/app.py.tmpl +7 -6
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/training/app.py.tmpl +14 -14
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/volume.py +20 -1
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/callbacks.py +15 -3
- {simulo-0.12.0 → simulo-0.13.0/src/simulo.egg-info}/PKG-INFO +22 -5
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo.egg-info/SOURCES.txt +5 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo.egg-info/requires.txt +5 -1
- simulo-0.12.0/src/simulo/_client/asset.py +0 -119
- simulo-0.12.0/src/simulo/_client/cli.py +0 -2364
- simulo-0.12.0/src/simulo/_client/runtime.py +0 -189
- {simulo-0.12.0 → simulo-0.13.0}/MANIFEST.in +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/setup.cfg +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/__init__.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/__init__.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/_entrypoint.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/_mounts.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/_runner.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/cancel_api.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/credentials.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/discovery.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/mode.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/registry.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/seed_ref.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/stub.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/__init__.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/inference/simuloignore.tmpl +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/scenario/simuloignore.tmpl +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/_client/view_session_api.py +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo/py.typed +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo.egg-info/dependency_links.txt +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo.egg-info/entry_points.txt +0 -0
- {simulo-0.12.0 → simulo-0.13.0}/src/simulo.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simulo
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: Simulo SDK and CLI — define robotics simulation and training apps in Python and run them on the Simulo cloud.
|
|
5
5
|
Author-email: Simulo Team <team@simulo.ai>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -15,8 +15,11 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Typing :: Typed
|
|
16
16
|
Requires-Python: >=3.11
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
|
-
Requires-Dist: simulo-interfaces<0.
|
|
18
|
+
Requires-Dist: simulo-interfaces<0.6,>=0.5
|
|
19
19
|
Requires-Dist: mcap<2,>=1.3
|
|
20
|
+
Requires-Dist: defusedxml>=0.7.1
|
|
21
|
+
Provides-Extra: assets
|
|
22
|
+
Requires-Dist: usd-core<27,>=25.5; extra == "assets"
|
|
20
23
|
Provides-Extra: dev
|
|
21
24
|
Requires-Dist: black>=24.0; extra == "dev"
|
|
22
25
|
Requires-Dist: isort>=5.13; extra == "dev"
|
|
@@ -52,7 +55,7 @@ The fastest way to start: scaffold a runnable app.
|
|
|
52
55
|
|
|
53
56
|
```bash
|
|
54
57
|
simulo create mybot
|
|
55
|
-
# Created mybot/app.py
|
|
58
|
+
# Created training app in mybot/ (app.py + .simuloignore).
|
|
56
59
|
cd mybot
|
|
57
60
|
```
|
|
58
61
|
|
|
@@ -69,8 +72,7 @@ run it.
|
|
|
69
72
|
# app.py
|
|
70
73
|
import simulo
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
app = simulo.App("hello", runtime=runtime)
|
|
75
|
+
app = simulo.App("hello")
|
|
74
76
|
|
|
75
77
|
|
|
76
78
|
@app.job(timeout=5 * 60)
|
|
@@ -100,4 +102,19 @@ no-op. To continue training from an earlier job's checkpoint instead, pass
|
|
|
100
102
|
`--from <job-ref>[:best|:latest]`. Job ids, and their unambiguous prefixes (`>=4` characters),
|
|
101
103
|
work anywhere the CLI takes a job id.
|
|
102
104
|
|
|
105
|
+
## Monitor jobs and retrieve results
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
simulo jobs # your jobs and their statuses
|
|
109
|
+
simulo logs --follow # stream the latest job's logs live
|
|
110
|
+
simulo result # the completed job's result JSON
|
|
111
|
+
simulo models # list / download trained checkpoints (sha256-verified)
|
|
112
|
+
simulo recordings # download MCAP flight recordings
|
|
113
|
+
simulo view # watch the live 3D scene (jobs submitted with --viewstream)
|
|
114
|
+
simulo cancel # cancel a queued or running job
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Commands that take a job id default to the latest job, announcing what they
|
|
118
|
+
resolved to on stderr.
|
|
119
|
+
|
|
103
120
|
Learn more at [simulo.ai](https://simulo.ai).
|
|
@@ -20,7 +20,7 @@ The fastest way to start: scaffold a runnable app.
|
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
22
|
simulo create mybot
|
|
23
|
-
# Created mybot/app.py
|
|
23
|
+
# Created training app in mybot/ (app.py + .simuloignore).
|
|
24
24
|
cd mybot
|
|
25
25
|
```
|
|
26
26
|
|
|
@@ -37,8 +37,7 @@ run it.
|
|
|
37
37
|
# app.py
|
|
38
38
|
import simulo
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
app = simulo.App("hello", runtime=runtime)
|
|
40
|
+
app = simulo.App("hello")
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
@app.job(timeout=5 * 60)
|
|
@@ -68,4 +67,19 @@ no-op. To continue training from an earlier job's checkpoint instead, pass
|
|
|
68
67
|
`--from <job-ref>[:best|:latest]`. Job ids, and their unambiguous prefixes (`>=4` characters),
|
|
69
68
|
work anywhere the CLI takes a job id.
|
|
70
69
|
|
|
70
|
+
## Monitor jobs and retrieve results
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
simulo jobs # your jobs and their statuses
|
|
74
|
+
simulo logs --follow # stream the latest job's logs live
|
|
75
|
+
simulo result # the completed job's result JSON
|
|
76
|
+
simulo models # list / download trained checkpoints (sha256-verified)
|
|
77
|
+
simulo recordings # download MCAP flight recordings
|
|
78
|
+
simulo view # watch the live 3D scene (jobs submitted with --viewstream)
|
|
79
|
+
simulo cancel # cancel a queued or running job
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Commands that take a job id default to the latest job, announcing what they
|
|
83
|
+
resolved to on stderr.
|
|
84
|
+
|
|
71
85
|
Learn more at [simulo.ai](https://simulo.ai).
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "simulo"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13.0"
|
|
8
8
|
description = "Simulo SDK and CLI — define robotics simulation and training apps in Python and run them on the Simulo cloud."
|
|
9
9
|
readme = "PYPI.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -25,9 +25,32 @@ classifiers = [
|
|
|
25
25
|
# guard, resolved only at job-execution time — never in this distribution.
|
|
26
26
|
# mcap: used by `simulo recordings` to read back a downloaded recording and
|
|
27
27
|
# verify it (message count) — MIT-licensed, deps only lz4+zstandard.
|
|
28
|
-
dependencies = [
|
|
28
|
+
dependencies = [
|
|
29
|
+
"simulo-interfaces>=0.5,<0.6",
|
|
30
|
+
"mcap>=1.3,<2",
|
|
31
|
+
# Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
|
|
32
|
+
# loop 2, security NIT): stdlib xml.etree.ElementTree relies on
|
|
33
|
+
# libexpat's own (practical-but-not-contractual) entity-expansion
|
|
34
|
+
# defaults to stay XXE/billion-laughs-safe; defusedxml pins that
|
|
35
|
+
# protection explicitly. Pure Python, zero transitive dependencies,
|
|
36
|
+
# ~25 KB — genuinely "cheap": adding it does not compromise the
|
|
37
|
+
# featherweight/no-USD-extra promise below (that promise is about
|
|
38
|
+
# avoiding usd-core's tens of MB, not about being literally
|
|
39
|
+
# zero-dependency).
|
|
40
|
+
"defusedxml>=0.7.1",
|
|
41
|
+
]
|
|
29
42
|
|
|
30
43
|
[project.optional-dependencies]
|
|
44
|
+
# `assets` — the FIRST user-facing extras group: USD package inspection for
|
|
45
|
+
# `simulo asset publish`/`validate` of USD trees (dependency closure + entry
|
|
46
|
+
# detection via pxr.UsdUtils). usd-core is simulator-free but tens of MB, so
|
|
47
|
+
# it must never tax users who don't publish USD: the import is lazy, ONLY in
|
|
48
|
+
# the USD inspection path (`_client/asset_package.py::_load_pxr_usdutils`),
|
|
49
|
+
# and a missing extra fails with the copyable `pip install "simulo[assets]"`.
|
|
50
|
+
# URDF publishing needs no OPT-IN extra (defusedxml + path math, both tiny
|
|
51
|
+
# always-installed base dependencies above — see that dependency's own
|
|
52
|
+
# comment).
|
|
53
|
+
assets = ["usd-core>=25.5,<27"]
|
|
31
54
|
dev = ["black>=24.0", "isort>=5.13", "ruff>=0.5", "mypy>=1.10", "pytest>=8.2", "build>=1.2", "twine>=5.1"]
|
|
32
55
|
# `dev` carries build/twine but deliberately not commitizen; `release` below
|
|
33
56
|
# adds commitizen for cutting version bumps without a second build/twine copy.
|
|
@@ -71,6 +94,7 @@ addopts = "--import-mode=importlib"
|
|
|
71
94
|
markers = [
|
|
72
95
|
"isaac: requires NVIDIA Isaac Sim / IsaacLab (skipped on dev machines and in PR CI)",
|
|
73
96
|
"gpu: requires a CUDA GPU (skipped on dev machines and in PR CI)",
|
|
97
|
+
"requires_execution_backend: end-to-end test that needs a live execution backend in the loop; skipped in CPU/unit runs",
|
|
74
98
|
]
|
|
75
99
|
|
|
76
100
|
[tool.isort]
|
|
@@ -34,6 +34,7 @@ from typing import Any, Callable, Iterator, Mapping, Optional, Sequence, TypeVar
|
|
|
34
34
|
|
|
35
35
|
from simulo._client import config, credentials, registry
|
|
36
36
|
from simulo._client._runner import JobFailedError, JobHandle, JobResultUnavailable
|
|
37
|
+
from simulo._client.runtime import DEFAULT_RUNTIME
|
|
37
38
|
from simulo.interfaces.ids import JobId
|
|
38
39
|
from simulo.interfaces.platform import JobCallbackProtocol, Mount
|
|
39
40
|
from simulo.interfaces.platform.enums import ResumePolicy
|
|
@@ -135,6 +136,44 @@ def seed_from_requested(seed_ref: Optional[str]) -> Iterator[None]:
|
|
|
135
136
|
_SEED_FROM_REF = previous
|
|
136
137
|
|
|
137
138
|
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Asset-pin ambient signals — `simulo run --frozen` / `--strict-assets` scope
|
|
141
|
+
# these for one submit, exactly like `--viewstream` / `--from` above and for the
|
|
142
|
+
# same "the spawn() is deep inside the user's @app.local_entrypoint" reason. They
|
|
143
|
+
# govern submit-time asset pin RESOLUTION (§6), which — like viewstream/seed —
|
|
144
|
+
# happens only on the cloud path (a local-disk submit has no control plane to
|
|
145
|
+
# resolve against) and NEVER touches `args` or the manifest, so they can't move
|
|
146
|
+
# `package_id`'s sha256.
|
|
147
|
+
# ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
_FROZEN_ASSETS_REQUESTED = False
|
|
150
|
+
_STRICT_ASSETS_REQUESTED = False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@contextlib.contextmanager
|
|
154
|
+
def frozen_assets_requested(enabled: bool) -> Iterator[None]:
|
|
155
|
+
"""Scope ``--frozen`` (fail on any unpinned asset ref) for one submit call."""
|
|
156
|
+
global _FROZEN_ASSETS_REQUESTED
|
|
157
|
+
previous = _FROZEN_ASSETS_REQUESTED
|
|
158
|
+
_FROZEN_ASSETS_REQUESTED = enabled
|
|
159
|
+
try:
|
|
160
|
+
yield
|
|
161
|
+
finally:
|
|
162
|
+
_FROZEN_ASSETS_REQUESTED = previous
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@contextlib.contextmanager
|
|
166
|
+
def strict_assets_requested(enabled: bool) -> Iterator[None]:
|
|
167
|
+
"""Scope ``--strict-assets`` (asset warnings become errors) for one submit call."""
|
|
168
|
+
global _STRICT_ASSETS_REQUESTED
|
|
169
|
+
previous = _STRICT_ASSETS_REQUESTED
|
|
170
|
+
_STRICT_ASSETS_REQUESTED = enabled
|
|
171
|
+
try:
|
|
172
|
+
yield
|
|
173
|
+
finally:
|
|
174
|
+
_STRICT_ASSETS_REQUESTED = previous
|
|
175
|
+
|
|
176
|
+
|
|
138
177
|
def _require_no_seed_for_local_submit(reason: str) -> None:
|
|
139
178
|
"""Refuse a local-disk submit that carries an ambient ``--from`` seed."""
|
|
140
179
|
if _SEED_FROM_REF is None:
|
|
@@ -210,6 +249,8 @@ def _spawn_cloud(
|
|
|
210
249
|
*,
|
|
211
250
|
viewstream: bool = False,
|
|
212
251
|
seed_from: Optional[str] = None,
|
|
252
|
+
frozen_assets: bool = False,
|
|
253
|
+
strict_assets: bool = False,
|
|
213
254
|
) -> JobHandle:
|
|
214
255
|
"""Tar the bundle, upload it, create the job, and return a cloud JobHandle.
|
|
215
256
|
|
|
@@ -227,7 +268,16 @@ def _spawn_cloud(
|
|
|
227
268
|
full source-job UUID — the confirmation that a typed prefix resolved to
|
|
228
269
|
the job the user meant); when an older server ignores it, ``create_job``
|
|
229
270
|
raises rather than let a fresh run masquerade as a seeded one.
|
|
271
|
+
|
|
272
|
+
Registry-asset pins (§6) follow the SAME sibling-of-``args`` rule. Every
|
|
273
|
+
``Asset.from_registry`` ref the app mounted is resolved to an exact
|
|
274
|
+
version in one batch BEFORE the package is uploaded — so a bad/missing/
|
|
275
|
+
unpinned ref fails in seconds rather than after the upload — the
|
|
276
|
+
``Resolved assets:`` block is printed, ``--frozen`` / ``--strict-assets``
|
|
277
|
+
are enforced, and the resolved ``[{ref, digest}]`` pins ride the
|
|
278
|
+
job-create call's top-level ``assets`` field, never ``args``.
|
|
230
279
|
"""
|
|
280
|
+
from simulo._client.asset_pins import DEFAULT_ASSET_RUNTIME, collect_asset_refs, resolve_and_pin_assets
|
|
231
281
|
from simulo._client.packaging import MANIFEST_FILENAME, ensure_tar
|
|
232
282
|
from simulo._client.submit_api import SubmitApiClient
|
|
233
283
|
|
|
@@ -261,6 +311,18 @@ def _spawn_cloud(
|
|
|
261
311
|
token = creds["access_token"] if creds else os.environ.get("SIMULO_API_TOKEN")
|
|
262
312
|
client = SubmitApiClient(base_url, token=token)
|
|
263
313
|
|
|
314
|
+
# Resolve every mounted registry-asset ref to an exact pin FIRST — before the
|
|
315
|
+
# package is uploaded — so a missing/unpinned/unvalidated ref fails in seconds
|
|
316
|
+
# (§6). Returns the top-level `assets: [{ref, digest}]` for create_job; `[]`
|
|
317
|
+
# when the app mounts no registry assets (no resolve call is made then).
|
|
318
|
+
asset_pins = resolve_and_pin_assets(
|
|
319
|
+
client,
|
|
320
|
+
collect_asset_refs(manifest),
|
|
321
|
+
runtime=DEFAULT_ASSET_RUNTIME,
|
|
322
|
+
frozen=frozen_assets,
|
|
323
|
+
strict=strict_assets,
|
|
324
|
+
)
|
|
325
|
+
|
|
264
326
|
client.create_package(
|
|
265
327
|
package_id=package_id,
|
|
266
328
|
source_digest=source_digest,
|
|
@@ -275,6 +337,7 @@ def _spawn_cloud(
|
|
|
275
337
|
args=args,
|
|
276
338
|
viewstream=viewstream,
|
|
277
339
|
seed_from_job_id=seed_from,
|
|
340
|
+
assets=asset_pins,
|
|
278
341
|
)
|
|
279
342
|
|
|
280
343
|
if seed_from is not None:
|
|
@@ -291,7 +354,13 @@ def _spawn_cloud(
|
|
|
291
354
|
|
|
292
355
|
|
|
293
356
|
class JobFunction:
|
|
294
|
-
"""
|
|
357
|
+
"""What ``@app.job`` returns: the original function, wrapped for submission.
|
|
358
|
+
|
|
359
|
+
Calling it runs the body in-process, exactly as if it were undecorated (that
|
|
360
|
+
is also how the platform invokes it on a worker). :meth:`spawn` and
|
|
361
|
+
:meth:`submit` package and submit it instead. You never construct one
|
|
362
|
+
yourself — decorate a function and use it from your ``@app.local_entrypoint``.
|
|
363
|
+
"""
|
|
295
364
|
|
|
296
365
|
def __init__(self, app: "App", spec: JobSpec) -> None:
|
|
297
366
|
self._app = app
|
|
@@ -307,32 +376,47 @@ class JobFunction:
|
|
|
307
376
|
return self._spec.fn(*args, **kwargs)
|
|
308
377
|
|
|
309
378
|
def spawn(self, **kwargs: Any) -> JobHandle:
|
|
310
|
-
"""Submit this job
|
|
379
|
+
"""Submit this job to the Simulo cloud (once logged in), or write it locally.
|
|
311
380
|
|
|
312
381
|
Submitting writes the manifest + source bundle (memoised once per app),
|
|
313
|
-
recording this job's name and ``kwargs`` into the manifest so the
|
|
314
|
-
|
|
382
|
+
recording this job's name and ``kwargs`` into the manifest so the job can
|
|
383
|
+
be run later with ``fn(**kwargs)``.
|
|
315
384
|
|
|
316
|
-
**Cloud mode** — credentials exist, or
|
|
317
|
-
names a target (and ``SIMULO_SUBMIT`` is
|
|
318
|
-
tarred, uploaded, and a job is created on
|
|
319
|
-
returned :class:`JobHandle` carries the
|
|
320
|
-
``.get()`` streams the real result.
|
|
385
|
+
**Cloud mode** — credentials exist (``simulo login``), or
|
|
386
|
+
``SIMULO_API_URL``/``SIMULO_ENV`` names a target (and ``SIMULO_SUBMIT`` is
|
|
387
|
+
not ``local``): the package is tarred, uploaded, and a job is created on
|
|
388
|
+
the Simulo cloud. The returned :class:`JobHandle` carries the
|
|
389
|
+
SERVER-assigned ``job_id`` and ``.get()`` streams the real result.
|
|
321
390
|
|
|
322
391
|
**Local-disk mode** (the default with no cloud signal present): NEVER
|
|
323
|
-
trains and
|
|
324
|
-
``.package_path`` / ``.job_id`` (a locally-derived id)
|
|
325
|
-
|
|
392
|
+
trains and never runs the job — the handle exposes only
|
|
393
|
+
``.package_path`` / ``.job_id`` (a locally-derived id), and ``.get()``
|
|
394
|
+
raises, pointing you at ``simulo login`` to submit to the cloud instead.
|
|
326
395
|
|
|
327
396
|
Positional arguments are rejected: a job body receives only the keyword
|
|
328
397
|
arguments recorded in the manifest.
|
|
329
398
|
"""
|
|
330
399
|
# Lazy import keeps app.py free of a module-scope dependency on the
|
|
331
400
|
# packager (and any import cycle); spawn is a runtime call.
|
|
401
|
+
from simulo._client.asset import current_captured_assets
|
|
332
402
|
from simulo._client.bundle import ensure_bundle
|
|
333
403
|
|
|
334
404
|
app_file, project_root = _app_source_location(self._spec.module)
|
|
335
|
-
|
|
405
|
+
# Construction-capture (freeze doc "PR-9 amendments"): every Asset
|
|
406
|
+
# constructed so far in the active `simulo run` capture scope (armed
|
|
407
|
+
# around the whole discovery import + entrypoint run — see cli.py) is
|
|
408
|
+
# unioned into this bundle's `resources`, even when never explicitly
|
|
409
|
+
# mounted via App(mounts=...) — an unmounted §7.1 handle
|
|
410
|
+
# (`forklift = simulo.Asset.from_registry(...)`, used only via
|
|
411
|
+
# `Robot(asset=forklift)`) must still get resolved and pinned.
|
|
412
|
+
package_path = ensure_bundle(
|
|
413
|
+
self._app,
|
|
414
|
+
app_file,
|
|
415
|
+
project_root,
|
|
416
|
+
job_name=self._spec.name,
|
|
417
|
+
args=dict(kwargs),
|
|
418
|
+
extra_assets=current_captured_assets(),
|
|
419
|
+
)
|
|
336
420
|
|
|
337
421
|
# SIMULO_SUBMIT=local is checked FIRST, before touching credentials at
|
|
338
422
|
# all — the escape hatch must short-circuit even when the saved token
|
|
@@ -351,6 +435,8 @@ class JobFunction:
|
|
|
351
435
|
creds,
|
|
352
436
|
viewstream=_VIEWSTREAM_REQUESTED,
|
|
353
437
|
seed_from=_SEED_FROM_REF,
|
|
438
|
+
frozen_assets=_FROZEN_ASSETS_REQUESTED,
|
|
439
|
+
strict_assets=_STRICT_ASSETS_REQUESTED,
|
|
354
440
|
)
|
|
355
441
|
else:
|
|
356
442
|
_require_no_seed_for_local_submit("no cloud target is configured")
|
|
@@ -444,19 +530,50 @@ def _arm_nothing_submitted_hint(app: "App") -> None:
|
|
|
444
530
|
|
|
445
531
|
|
|
446
532
|
class App:
|
|
447
|
-
"""
|
|
533
|
+
"""The user-facing job container — declare it once, at module scope.
|
|
534
|
+
|
|
535
|
+
An app names your project and (optionally) mounts assets/volumes into
|
|
536
|
+
every job. Its jobs execute in a Simulo-curated runtime —
|
|
537
|
+
:data:`~simulo._client.runtime.DEFAULT_RUNTIME` unless the app picks a
|
|
538
|
+
different Simulo runtime via ``runtime=``. Decorate functions with
|
|
539
|
+
:meth:`job` to make them submittable, and one function with
|
|
540
|
+
:meth:`local_entrypoint` to define what ``simulo run app.py`` does::
|
|
541
|
+
|
|
542
|
+
import simulo
|
|
543
|
+
|
|
544
|
+
app = simulo.App("cartpole")
|
|
545
|
+
|
|
546
|
+
@app.job(gpu="A10G")
|
|
547
|
+
def train(iterations: int = 1000):
|
|
548
|
+
...
|
|
549
|
+
|
|
550
|
+
@app.local_entrypoint
|
|
551
|
+
def main(iterations: int = 1000):
|
|
552
|
+
train.spawn(iterations=iterations)
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
name: Non-empty app name, recorded in the package manifest.
|
|
556
|
+
runtime: Optionally pick a different Simulo runtime for this app's
|
|
557
|
+
jobs (``simulo.Runtime.from_registry("simulo/gpu-rl:2026.06")``).
|
|
558
|
+
Omitted means the platform default.
|
|
559
|
+
mounts: Optional mapping of mount name to :class:`simulo.Asset` /
|
|
560
|
+
:class:`simulo.Volume`, shared by every job of this app.
|
|
561
|
+
|
|
562
|
+
Raises:
|
|
563
|
+
ValueError: if ``name`` is empty or whitespace-only.
|
|
564
|
+
"""
|
|
448
565
|
|
|
449
566
|
def __init__(
|
|
450
567
|
self,
|
|
451
568
|
name: str,
|
|
452
569
|
*,
|
|
453
|
-
runtime: object,
|
|
570
|
+
runtime: Optional[object] = None,
|
|
454
571
|
mounts: Optional[Mapping[str, Mount]] = None,
|
|
455
572
|
) -> None:
|
|
456
573
|
if not name or not name.strip():
|
|
457
574
|
raise ValueError("App name must be a non-empty string.")
|
|
458
575
|
self._name = name
|
|
459
|
-
self._runtime = runtime
|
|
576
|
+
self._runtime = runtime if runtime is not None else DEFAULT_RUNTIME
|
|
460
577
|
self._mounts: dict[str, Mount] = dict(mounts or {})
|
|
461
578
|
self._jobs: dict[str, JobSpec] = {}
|
|
462
579
|
self._local_entrypoints: dict[str, Callable[..., object]] = {}
|
|
@@ -467,22 +584,27 @@ class App:
|
|
|
467
584
|
|
|
468
585
|
@property
|
|
469
586
|
def name(self) -> str:
|
|
587
|
+
"""The app's name, as passed at construction."""
|
|
470
588
|
return self._name
|
|
471
589
|
|
|
472
590
|
@property
|
|
473
591
|
def runtime(self) -> Any:
|
|
592
|
+
"""The Simulo runtime this app's jobs execute in (the default unless picked)."""
|
|
474
593
|
return self._runtime
|
|
475
594
|
|
|
476
595
|
@property
|
|
477
596
|
def mounts(self) -> Mapping[str, Mount]:
|
|
597
|
+
"""The assets/volumes mounted into every job of this app, by mount name (a copy)."""
|
|
478
598
|
return dict(self._mounts)
|
|
479
599
|
|
|
480
600
|
@property
|
|
481
601
|
def jobs(self) -> Mapping[str, JobSpec]:
|
|
602
|
+
"""The registered job specs, by job name (a copy)."""
|
|
482
603
|
return dict(self._jobs)
|
|
483
604
|
|
|
484
605
|
@property
|
|
485
606
|
def local_entrypoint_names(self) -> tuple[str, ...]:
|
|
607
|
+
"""Names of the functions registered with ``@app.local_entrypoint``."""
|
|
486
608
|
return tuple(self._local_entrypoints)
|
|
487
609
|
|
|
488
610
|
@overload
|
|
@@ -511,7 +633,52 @@ class App:
|
|
|
511
633
|
retries: int = 0,
|
|
512
634
|
**extra: Any,
|
|
513
635
|
) -> Union[JobFunction, Callable[[F], JobFunction]]:
|
|
514
|
-
"""Register a job
|
|
636
|
+
"""Register a function as a job of this app. Records metadata; never runs the body.
|
|
637
|
+
|
|
638
|
+
Usable bare (``@app.job``) or with options (``@app.job(gpu="A10G")``).
|
|
639
|
+
Returns a :class:`JobFunction` wrapping the original function — calling
|
|
640
|
+
it still runs the body in-process; ``.spawn()`` / ``.submit()`` submit it
|
|
641
|
+
to the platform instead.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
fn: The function being decorated (bare-decorator form only — never
|
|
645
|
+
pass it alongside keyword options).
|
|
646
|
+
callbacks: Lifecycle callbacks from :mod:`simulo.callbacks` (e.g.
|
|
647
|
+
``ResumableCheckpoint``), recorded into the package manifest and
|
|
648
|
+
honored at execution.
|
|
649
|
+
resume: How the job resumes from a discovered checkpoint
|
|
650
|
+
(:class:`~simulo.interfaces.platform.enums.ResumePolicy`,
|
|
651
|
+
default ``AUTO``).
|
|
652
|
+
gpu: GPU resource request (e.g. ``"A10G"``), recorded in the job's
|
|
653
|
+
resource spec.
|
|
654
|
+
timeout: Wall-clock limit for one managed execution, in seconds
|
|
655
|
+
(a positive integer; validated HERE, at decoration time — a
|
|
656
|
+
zero, negative, or non-integer value raises ``ValueError``
|
|
657
|
+
immediately rather than failing later at run time on the
|
|
658
|
+
worker). Enforced by the platform worker: a job still
|
|
659
|
+
running at the limit is killed and reported ``failed`` with
|
|
660
|
+
reason ``timeout``. When omitted (``None``, the default),
|
|
661
|
+
the platform ceiling applies (1 hour by default); a declared
|
|
662
|
+
value larger than the ceiling is clamped down to it. The
|
|
663
|
+
ceiling is an operator knob on the worker
|
|
664
|
+
(``MAX_JOB_TIMEOUT_S=<seconds>``) for long unattended
|
|
665
|
+
training.
|
|
666
|
+
retries: Automatic re-attempts after a failure (default 0). All
|
|
667
|
+
attempts share one ``timeout`` budget.
|
|
668
|
+
**extra: Additional resource requests, recorded verbatim in the
|
|
669
|
+
job's resource spec.
|
|
670
|
+
|
|
671
|
+
Raises:
|
|
672
|
+
ValueError: ``timeout`` is not ``None`` and is not a positive
|
|
673
|
+
integer (mirrors the worker's own
|
|
674
|
+
``worker._effective_job_timeout_s`` guard — bare ``bool`` is
|
|
675
|
+
excluded even though it is an ``int`` subclass).
|
|
676
|
+
"""
|
|
677
|
+
if timeout is not None and (isinstance(timeout, bool) or not isinstance(timeout, int) or timeout < 1):
|
|
678
|
+
raise ValueError(
|
|
679
|
+
"@app.job(timeout=...) must be a positive integer (seconds), or omitted/None to "
|
|
680
|
+
f"use the platform ceiling — got {timeout!r}."
|
|
681
|
+
)
|
|
515
682
|
|
|
516
683
|
def decorate(func: F) -> JobFunction:
|
|
517
684
|
resources: dict[str, Any] = {}
|