simulo 0.26.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.
Files changed (55) hide show
  1. simulo/__init__.py +433 -0
  2. simulo/_client/__init__.py +6 -0
  3. simulo/_client/_entrypoint.py +313 -0
  4. simulo/_client/_mounts.py +25 -0
  5. simulo/_client/_runner.py +186 -0
  6. simulo/_client/_secure_downloads.py +1181 -0
  7. simulo/_client/app.py +1308 -0
  8. simulo/_client/asset.py +331 -0
  9. simulo/_client/asset_api.py +517 -0
  10. simulo/_client/asset_package.py +1103 -0
  11. simulo/_client/asset_pins.py +187 -0
  12. simulo/_client/builtin_aliases.py +107 -0
  13. simulo/_client/bundle.py +254 -0
  14. simulo/_client/cancel_api.py +104 -0
  15. simulo/_client/cli.py +9063 -0
  16. simulo/_client/config.py +186 -0
  17. simulo/_client/credentials.py +210 -0
  18. simulo/_client/discovery.py +214 -0
  19. simulo/_client/export_api.py +212 -0
  20. simulo/_client/export_bundle.py +296 -0
  21. simulo/_client/facades.py +581 -0
  22. simulo/_client/http.py +414 -0
  23. simulo/_client/identity_api.py +117 -0
  24. simulo/_client/install_samples.py +267 -0
  25. simulo/_client/jobs_api.py +224 -0
  26. simulo/_client/learning.py +393 -0
  27. simulo/_client/login.py +319 -0
  28. simulo/_client/mode.py +29 -0
  29. simulo/_client/outputs.py +116 -0
  30. simulo/_client/packaging.py +445 -0
  31. simulo/_client/preflight_api.py +186 -0
  32. simulo/_client/preflight_render.py +200 -0
  33. simulo/_client/registry.py +98 -0
  34. simulo/_client/runtime.py +185 -0
  35. simulo/_client/runtime_display.py +90 -0
  36. simulo/_client/seed_ref.py +76 -0
  37. simulo/_client/stub.py +41 -0
  38. simulo/_client/submit_api.py +1057 -0
  39. simulo/_client/templates/__init__.py +21 -0
  40. simulo/_client/templates/inference/app.py.tmpl +316 -0
  41. simulo/_client/templates/inference/simuloignore.tmpl +30 -0
  42. simulo/_client/templates/scenario/app.py.tmpl +93 -0
  43. simulo/_client/templates/scenario/simuloignore.tmpl +27 -0
  44. simulo/_client/templates/training/app.py.tmpl +235 -0
  45. simulo/_client/templates/training/simuloignore.tmpl +29 -0
  46. simulo/_client/view_fragment.py +21 -0
  47. simulo/_client/view_session_api.py +122 -0
  48. simulo/_client/volume.py +71 -0
  49. simulo/callbacks.py +274 -0
  50. simulo/py.typed +0 -0
  51. simulo-0.26.0.dist-info/METADATA +130 -0
  52. simulo-0.26.0.dist-info/RECORD +55 -0
  53. simulo-0.26.0.dist-info/WHEEL +5 -0
  54. simulo-0.26.0.dist-info/entry_points.txt +2 -0
  55. simulo-0.26.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,331 @@
1
+ """``Asset`` — a read-only, registry-resolved mount (implements ``AssetProtocol``).
2
+
3
+ A registry asset ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
4
+ ``robot/so-arm-100:v3``, ``simulo/world/warehouse:v2``, or the unpinned
5
+ ``robot/forklift`` — resolved to an exact version **at submit**). The grammar
6
+ is the ONE shared parser from ``simulo.interfaces.platform.asset_catalog`` — the
7
+ CLI, the control plane, the worker, and the console all parse through it, so the
8
+ grammar can never drift. This handle carries the parsed ``kind`` (``robot`` /
9
+ ``world`` / ``prop``), which enables **kind safety** (``Robot(asset=<world asset>)`` fails
10
+ with a one-line explanation naming the mismatched kind).
11
+
12
+ The on-disk :attr:`path` only exists in **execution mode**:
13
+
14
+ * On a cloud worker (or a local ``run-package``), the runner exports
15
+ ``SIMULO_ASSET_<uri>`` pointing at the read-only mount; :attr:`path` returns it.
16
+ * When no runner mount is present (a local run against the on-disk catalog cache),
17
+ :attr:`path` resolves the asset out of ``~/.simulo/assets/<org>/<kind>/<name>/v<N>/``
18
+ — the exact colon-free, NTFS-safe layout ``simulo asset get`` writes into — and,
19
+ when the asset has not been fetched, raises with the copyable
20
+ ``simulo asset get <ref>`` command to run.
21
+
22
+ ``Asset`` also unifies the *model-asset* spec surface: ``usd(path)`` / ``urdf(path)``
23
+ name a robot/object source to load into a scene. In discovery (submit) these return
24
+ an inert, torch-free :class:`_AssetSpec` so authoring a task is lean; only the
25
+ backend runner (execution mode) resolves them to the real model-source
26
+ ``simulo.interfaces.authoring.asset.AssetSource`` (the single, dependency-free
27
+ definition the backend consumes — relocated out of the backend in Plan C, renamed
28
+ from ``Asset`` by owner decision to resolve the name collision with this very class).
29
+
30
+ **Layer split.** This is the user-facing *handle* (``simulo.Asset``); the
31
+ asset-catalog **wire** contract (ref grammar, routes, caps, wire dataclasses) lives
32
+ in ``simulo.interfaces.platform.asset_catalog`` — this module consumes that
33
+ module's :func:`parse_asset_ref` and stays decoupled from it otherwise.
34
+ """
35
+
36
+ import contextlib
37
+ import os
38
+ from dataclasses import dataclass
39
+ from pathlib import Path
40
+ from typing import Any, Iterator, List, Optional, Tuple
41
+
42
+ from simulo._client._mounts import asset_env_key
43
+ from simulo._client.mode import EXECUTION, current_mode
44
+ from simulo.interfaces.authoring import AssetSource as _ModelSourceAsset
45
+ from simulo.interfaces.exceptions import ContractViolationError, ResourceNotFoundError
46
+ from simulo.interfaces.platform.asset_catalog import (
47
+ GLOBAL_ASSET_PUBLISHER,
48
+ RESERVED_ASSET_PUBLISHERS,
49
+ ParsedAssetRef,
50
+ format_asset_ref,
51
+ parse_asset_ref,
52
+ )
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Construction capture — every `Asset.from_registry(...)` handle constructed
56
+ # while a capture scope is active is recorded here (the viewstream/seed
57
+ # ambient-signal pattern in `app.py`, applied to CONSTRUCTION rather than a
58
+ # submit flag). This is what lets an UNMOUNTED registry handle
59
+ # (`forklift = simulo.Asset.from_registry(...)`, used only via
60
+ # `Robot(asset=forklift)`, never passed to `App(mounts=...)`) still get
61
+ # resolved and pinned: `simulo run` arms this scope BEFORE importing the app
62
+ # file (so module-level constructions are seen) and keeps it armed through the
63
+ # whole entrypoint invocation. `bundle.build_manifest` unions the captured
64
+ # assets into the manifest's `resources` list alongside the App's own
65
+ # `mounts=`-declared ones (authored-ref mount invariant — "Handle collection
66
+ # is CONSTRUCTION-CAPTURE, not mount-only").
67
+ # ---------------------------------------------------------------------------
68
+
69
+ _CONSTRUCTION_CAPTURE: Optional[List["Asset"]] = None
70
+
71
+
72
+ @contextlib.contextmanager
73
+ def capture_constructed_assets() -> Iterator[List["Asset"]]:
74
+ """Collect every :class:`Asset` constructed in this scope (nestable; the
75
+ innermost active scope receives each construction)."""
76
+ global _CONSTRUCTION_CAPTURE
77
+ previous = _CONSTRUCTION_CAPTURE
78
+ captured: List["Asset"] = []
79
+ _CONSTRUCTION_CAPTURE = captured
80
+ try:
81
+ yield captured
82
+ finally:
83
+ _CONSTRUCTION_CAPTURE = previous
84
+
85
+
86
+ def current_captured_assets() -> Tuple["Asset", ...]:
87
+ """Every :class:`Asset` constructed so far in the active capture scope, or
88
+ ``()`` when no scope is active (e.g. a direct ``JobFunction.spawn()`` call
89
+ outside ``simulo run`` — that path still resolves whatever the App's own
90
+ ``mounts=`` declares; construction-capture only ADDS coverage for
91
+ unmounted handles, it is never the sole source of truth)."""
92
+ if _CONSTRUCTION_CAPTURE is None:
93
+ return ()
94
+ return tuple(_CONSTRUCTION_CAPTURE)
95
+
96
+
97
+ @dataclass(frozen=True)
98
+ class _AssetSpec:
99
+ """Inert, torch-free model-asset spec produced in discovery mode.
100
+
101
+ Records the *kind* (``"usd"`` / ``"urdf"``) and source ``path`` a user named via
102
+ ``Asset.usd(...)`` / ``Asset.urdf(...)``. It is never used for computation at
103
+ submit — the real model-source asset is materialised on the worker.
104
+ """
105
+
106
+ kind: str
107
+ path: str
108
+
109
+
110
+ def _parse_registry_ref(uri: str) -> ParsedAssetRef:
111
+ """Parse a registry ref through the shared grammar, re-raising as a
112
+ :class:`ContractViolationError` (the SDK's user-facing input-error type).
113
+
114
+ The wire grammar raises :class:`ValueError` with a user-language-first
115
+ message; the SDK surface speaks ``ContractViolationError``, so the message is
116
+ preserved and only the type is adapted — a bad ref reads the same whether it
117
+ reaches the user through the CLI resolver or this handle.
118
+ """
119
+ try:
120
+ return parse_asset_ref(uri)
121
+ except ValueError as exc:
122
+ raise ContractViolationError(str(exc)) from exc
123
+
124
+
125
+ def _asset_cache_root() -> Path:
126
+ """The local catalog cache root — ``~/.simulo/assets`` (what ``simulo asset
127
+ get`` writes into). Reads ``$HOME`` via :meth:`Path.home` so test isolation
128
+ (a redirected ``HOME``) is honored in-process, exactly like the credentials
129
+ path."""
130
+ return Path.home() / ".simulo" / "assets"
131
+
132
+
133
+ class Asset:
134
+ """A read-only, registry-resolved resource mounted into running jobs.
135
+
136
+ A registry ref is ``[<publisher>/]<kind>/<name>[:v<N>]`` (e.g.
137
+ ``simulo/robot/cartpole:v1``); :meth:`from_registry` names one, and :attr:`path`
138
+ resolves to its read-only mount inside a running job. ``Asset`` is also how a
139
+ task names USD/URDF model sources to load into a scene — :meth:`usd` /
140
+ :meth:`urdf`.
141
+
142
+ Raises:
143
+ ContractViolationError: if ``uri`` is not a valid
144
+ ``[<publisher>/]<kind>/<name>[:v<N>]`` ref.
145
+ """
146
+
147
+ def __init__(self, uri: str, *, read_only: bool = True) -> None:
148
+ self._ref = _parse_registry_ref(uri)
149
+ self._uri = uri
150
+ self._read_only = read_only
151
+ if _CONSTRUCTION_CAPTURE is not None:
152
+ _CONSTRUCTION_CAPTURE.append(self)
153
+
154
+ @classmethod
155
+ def from_registry(cls, uri: str, read_only: bool = True) -> "Asset":
156
+ """Get a handle to the registry asset at ``uri``.
157
+
158
+ ``uri`` is a catalog ref ``[<publisher>/]<kind>/<name>[:v<N>]`` — the
159
+ publisher defaults to your own org, ``simulo`` names the global catalog,
160
+ and an omitted ``:v<N>`` is resolved to the current latest **at submit**.
161
+ The handle is directly consumable wherever a model source is
162
+ accepted — ``Robot(asset=...)`` for a ``robot`` ref, ``scene.add(...)``
163
+ for a ``world`` ref.
164
+ """
165
+ return cls(uri, read_only=read_only)
166
+
167
+ @classmethod
168
+ def usd(cls, path: str) -> Any:
169
+ """Name a USD model source to load into a scene.
170
+
171
+ At submit time this returns an inert, torch-free spec object so authoring
172
+ stays lean; in the execution runtime it resolves to the real model asset. The return
173
+ type depends on mode, so it is typed ``Any``.
174
+ """
175
+ return cls._model_asset("usd", path)
176
+
177
+ @classmethod
178
+ def urdf(cls, path: str) -> Any:
179
+ """Name a URDF model source to load into a scene (see :meth:`usd`)."""
180
+ return cls._model_asset("urdf", path)
181
+
182
+ @staticmethod
183
+ def _model_asset(kind: str, path: str) -> Any:
184
+ if current_mode() == EXECUTION:
185
+ # The model-source AssetSource is the dependency-free
186
+ # ``simulo.interfaces.authoring.asset.AssetSource`` (single definition,
187
+ # relocated out of the backend in Plan C) — importable statically
188
+ # and torch-free, so no lazy import is needed any more.
189
+ factory = getattr(_ModelSourceAsset, kind)
190
+ return factory(path)
191
+ return _AssetSpec(kind=kind, path=path)
192
+
193
+ @property
194
+ def uri(self) -> str:
195
+ """The registry ref as authored (``[<publisher>/]<kind>/<name>[:v<N>]``)."""
196
+ return self._uri
197
+
198
+ @property
199
+ def kind(self) -> str:
200
+ """The ``<kind>`` segment of the ref — ``"robot"``, ``"world"``, or ``"prop"``."""
201
+ return self._ref.kind
202
+
203
+ @property
204
+ def name(self) -> str:
205
+ """The asset ``<name>`` slug."""
206
+ return self._ref.name
207
+
208
+ @property
209
+ def publisher(self) -> Optional[str]:
210
+ """The owning catalog publisher (``"simulo"`` = global, an org slug, or
211
+ ``None`` when omitted — the caller's own org)."""
212
+ return self._ref.publisher
213
+
214
+ @property
215
+ def version(self) -> Optional[int]:
216
+ """The pinned version number, or ``None`` for an unpinned ref (resolved at
217
+ submit)."""
218
+ return self._ref.version
219
+
220
+ @property
221
+ def is_read_only(self) -> bool:
222
+ """Whether this handle is read-only (registry assets always are)."""
223
+ return self._read_only
224
+
225
+ def read_only(self) -> "Asset":
226
+ """Return a read-only view of this asset (builder marker)."""
227
+ return Asset(self._uri, read_only=True)
228
+
229
+ def require_kind(self, expected: str) -> None:
230
+ """Fail with a one-line explanation if this asset's kind is not ``expected``.
231
+
232
+ The consumption-side half of **kind safety**: a ``robot`` context
233
+ (``Robot(asset=...)``) requires a ``robot`` asset, a ``world`` context a
234
+ ``world`` asset. ``simulo.core.Robot`` calls this (duck-typed) so
235
+ ``Robot(asset=simulo.Asset.from_registry("world/warehouse:v2"))`` fails
236
+ with a clear message rather than surfacing later as an opaque runtime
237
+ error. The kind is known from the ref itself, so the check is a cheap
238
+ string comparison — no catalog lookup.
239
+ """
240
+ if self._ref.kind == expected:
241
+ return
242
+ message = (
243
+ f"asset {self._uri!r} is a {self._ref.kind!r} asset, but a {expected!r} asset is required here — "
244
+ f"use a {expected}/... ref."
245
+ )
246
+ if expected == "robot":
247
+ # The common mix-up: a world handed to Robot(asset=...). Name the right
248
+ # consumption site so the fix is obvious.
249
+ message += " A world asset is added to the scene with scene.add(...), not built as a robot."
250
+ raise ContractViolationError(message)
251
+
252
+ @property
253
+ def path(self) -> str:
254
+ """Local read-only mount path — only meaningful inside a running job.
255
+
256
+ Resolution order, in execution mode:
257
+
258
+ 1. ``SIMULO_ASSET_<uri>`` — the mount the execution environment
259
+ exported for this asset. When set, it is authoritative.
260
+ 2. The on-disk catalog cache ``~/.simulo/assets/<org>/<kind>/<name>/v<N>/``
261
+ — the colon-free layout ``simulo asset get`` writes into. Used for a
262
+ local run with no runner-provided mount.
263
+
264
+ Raises outside execution mode, or — when neither a mount env var nor a
265
+ cached copy is present — with the exact ``simulo asset get <ref>`` command
266
+ to fetch it.
267
+ """
268
+ if current_mode() != EXECUTION:
269
+ raise ResourceNotFoundError(
270
+ f"Asset {self._uri!r}.path is only available inside a running job "
271
+ f"(SIMULO_MODE={EXECUTION!r}); it is not resolvable during packaging."
272
+ )
273
+ mount = os.environ.get(asset_env_key(self._uri))
274
+ if mount is not None:
275
+ return mount
276
+ cached = self._resolve_from_cache()
277
+ if cached is not None:
278
+ return str(cached)
279
+ raise ResourceNotFoundError(
280
+ f"Asset {self._uri!r} is not mounted and no local copy was found. Fetch it first:\n"
281
+ f" simulo asset get {self._uri}"
282
+ )
283
+
284
+ def _resolve_from_cache(self) -> Optional[Path]:
285
+ """Find this asset in ``~/.simulo/assets/<org>/<kind>/<name>/v<N>/``, or ``None``.
286
+
287
+ **No silent cross-catalog fallback** (authored-ref mount invariant): an
288
+ explicit-publisher ref (``simulo`` for the global catalog, or an org slug)
289
+ resolves under that ONE catalog directory only. An omitted-publisher ref
290
+ means the caller's OWN org and NOTHING ELSE — every :data:`RESERVED_ASSET_PUBLISHERS`
291
+ directory (chief among them ``simulo``, the global catalog) is excluded from
292
+ its glob, so a cached global copy can never silently satisfy an own-org ref.
293
+ The caller's actual org slug is not resolvable client-side (saved
294
+ credentials carry only ``active_organization_id``, a UUID, never a slug),
295
+ so "not a reserved word" is the best available own-org filter.
296
+
297
+ Zero or ambiguous (``> 1``) matches resolve to ``None`` (the caller renders
298
+ the generic ``simulo asset get <ref>`` hint). The one exception: an
299
+ omitted-publisher ref with ZERO own-org matches but a cache hit under the
300
+ GLOBAL catalog raises directly here, naming the exact global ref — a
301
+ cross-catalog hit is real, useful information, not a plain miss.
302
+ """
303
+ root = _asset_cache_root()
304
+ version_glob = f"v{self._ref.version}" if self._ref.version is not None else "v*"
305
+
306
+ if self._ref.publisher is not None:
307
+ pattern = f"{self._ref.publisher}/{self._ref.kind}/{self._ref.name}/{version_glob}"
308
+ matches = sorted(p for p in root.glob(pattern) if p.is_dir())
309
+ return matches[0] if len(matches) == 1 else None
310
+
311
+ pattern = f"*/{self._ref.kind}/{self._ref.name}/{version_glob}"
312
+ all_matches = sorted(p for p in root.glob(pattern) if p.is_dir())
313
+ own_org_matches = [p for p in all_matches if p.relative_to(root).parts[0] not in RESERVED_ASSET_PUBLISHERS]
314
+ if len(own_org_matches) == 1:
315
+ return own_org_matches[0]
316
+ if not own_org_matches:
317
+ global_matches = [p for p in all_matches if p.relative_to(root).parts[0] == GLOBAL_ASSET_PUBLISHER]
318
+ if global_matches:
319
+ hint_ref = format_asset_ref(
320
+ publisher=GLOBAL_ASSET_PUBLISHER,
321
+ kind=self._ref.kind,
322
+ name=self._ref.name,
323
+ version=int(global_matches[0].name.removeprefix("v")),
324
+ )
325
+ raise ResourceNotFoundError(
326
+ f"Asset {self._uri!r} has no local copy under your own org's cache, but the GLOBAL "
327
+ f"catalog has one cached. Catalogs never fall back to each other silently — if you "
328
+ f"meant the global asset, use its explicit ref:\n"
329
+ f" simulo asset get {hint_ref}"
330
+ )
331
+ return None # zero, or ambiguous (>1), own-org matches