simulo 0.26.0__tar.gz → 0.28.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 (61) hide show
  1. {simulo-0.26.0/src/simulo.egg-info → simulo-0.28.0}/PKG-INFO +15 -2
  2. {simulo-0.26.0 → simulo-0.28.0}/PYPI.md +13 -0
  3. {simulo-0.26.0 → simulo-0.28.0}/pyproject.toml +2 -2
  4. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/_runner.py +15 -3
  5. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/app.py +37 -1
  6. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/asset_api.py +1 -0
  7. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/bundle.py +2 -1
  8. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/cli.py +234 -59
  9. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/credentials.py +134 -53
  10. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/http.py +118 -3
  11. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/install_samples.py +138 -4
  12. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/packaging.py +10 -0
  13. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/preflight_api.py +9 -13
  14. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/preflight_render.py +36 -11
  15. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/submit_api.py +45 -28
  16. {simulo-0.26.0 → simulo-0.28.0/src/simulo.egg-info}/PKG-INFO +15 -2
  17. {simulo-0.26.0 → simulo-0.28.0}/src/simulo.egg-info/requires.txt +1 -1
  18. {simulo-0.26.0 → simulo-0.28.0}/MANIFEST.in +0 -0
  19. {simulo-0.26.0 → simulo-0.28.0}/setup.cfg +0 -0
  20. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/__init__.py +0 -0
  21. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/__init__.py +0 -0
  22. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/_entrypoint.py +0 -0
  23. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/_mounts.py +0 -0
  24. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/_secure_downloads.py +0 -0
  25. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/asset.py +0 -0
  26. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/asset_package.py +0 -0
  27. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/asset_pins.py +0 -0
  28. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/builtin_aliases.py +0 -0
  29. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/cancel_api.py +0 -0
  30. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/config.py +0 -0
  31. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/discovery.py +0 -0
  32. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/export_api.py +0 -0
  33. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/export_bundle.py +0 -0
  34. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/facades.py +0 -0
  35. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/identity_api.py +0 -0
  36. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/jobs_api.py +0 -0
  37. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/learning.py +0 -0
  38. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/login.py +0 -0
  39. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/mode.py +0 -0
  40. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/outputs.py +0 -0
  41. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/registry.py +0 -0
  42. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/runtime.py +0 -0
  43. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/runtime_display.py +0 -0
  44. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/seed_ref.py +0 -0
  45. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/stub.py +0 -0
  46. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/__init__.py +0 -0
  47. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/inference/app.py.tmpl +0 -0
  48. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/inference/simuloignore.tmpl +0 -0
  49. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/scenario/app.py.tmpl +0 -0
  50. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/scenario/simuloignore.tmpl +0 -0
  51. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/training/app.py.tmpl +0 -0
  52. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  53. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/view_fragment.py +0 -0
  54. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/view_session_api.py +0 -0
  55. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/_client/volume.py +0 -0
  56. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/callbacks.py +0 -0
  57. {simulo-0.26.0 → simulo-0.28.0}/src/simulo/py.typed +0 -0
  58. {simulo-0.26.0 → simulo-0.28.0}/src/simulo.egg-info/SOURCES.txt +0 -0
  59. {simulo-0.26.0 → simulo-0.28.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  60. {simulo-0.26.0 → simulo-0.28.0}/src/simulo.egg-info/entry_points.txt +0 -0
  61. {simulo-0.26.0 → simulo-0.28.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.26.0
3
+ Version: 0.28.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,7 +15,7 @@ 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.20,>=0.19.0
18
+ Requires-Dist: simulo-interfaces<0.21,>=0.20.0
19
19
  Requires-Dist: mcap<2,>=1.3
20
20
  Requires-Dist: defusedxml>=0.7.1
21
21
  Requires-Dist: usd-core<27,>=26.5
@@ -75,6 +75,19 @@ To download a curated collection of runnable projects, use `simulo install sampl
75
75
  It creates `./simulo-samples` as a normal Git clone, so `cd simulo-samples` and
76
76
  `cat README.md` open the sample index.
77
77
 
78
+ Choose a destination with `--directory` and a Git branch, tag, or commit with
79
+ `--ref`. You can use either option on its own or combine them:
80
+
81
+ ```bash
82
+ simulo install samples --directory robotics-examples --ref main
83
+ cd robotics-examples
84
+ ```
85
+
86
+ The destination must not exist, and its parent directory must already exist.
87
+ The directory appears only after the clone and requested checkout succeed.
88
+ Use a tag or full commit ID instead of `main` to select a fixed revision.
89
+ With `--ref`, the checkout is detached at the selected commit.
90
+
78
91
  ### Or write an app by hand
79
92
 
80
93
  ```python
@@ -35,6 +35,19 @@ To download a curated collection of runnable projects, use `simulo install sampl
35
35
  It creates `./simulo-samples` as a normal Git clone, so `cd simulo-samples` and
36
36
  `cat README.md` open the sample index.
37
37
 
38
+ Choose a destination with `--directory` and a Git branch, tag, or commit with
39
+ `--ref`. You can use either option on its own or combine them:
40
+
41
+ ```bash
42
+ simulo install samples --directory robotics-examples --ref main
43
+ cd robotics-examples
44
+ ```
45
+
46
+ The destination must not exist, and its parent directory must already exist.
47
+ The directory appears only after the clone and requested checkout succeed.
48
+ Use a tag or full commit ID instead of `main` to select a fixed revision.
49
+ With `--ref`, the checkout is detached at the selected commit.
50
+
38
51
  ### Or write an app by hand
39
52
 
40
53
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo"
7
- version = "0.26.0"
7
+ version = "0.28.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"
@@ -26,7 +26,7 @@ classifiers = [
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
28
  dependencies = [
29
- "simulo-interfaces>=0.19.0,<0.20",
29
+ "simulo-interfaces>=0.20.0,<0.21",
30
30
  "mcap>=1.3,<2",
31
31
  # Hardened XML parsing for URDF ingestion (USD Asset Catalogs, PR-12 fix
32
32
  # loop 2, security NIT): stdlib xml.etree.ElementTree relies on
@@ -27,14 +27,26 @@ from typing import Any, NoReturn, Optional
27
27
 
28
28
  from simulo.interfaces.ids import JobId, JobPublicId
29
29
  from simulo.interfaces.platform.enums import JobStatus
30
- from simulo.interfaces.platform.runs import JOB_SCOPE_MINE, TERMINAL_JOB_STATUSES
30
+ from simulo.interfaces.platform.runs import JOB_SCOPE_MINE
31
31
 
32
32
  #: Poll cadence for cloud ``.get()``. Module-level so tests can shrink it.
33
33
  _POLL_INTERVAL_S = 1.0
34
34
 
35
- _TERMINAL_STATUS_STRINGS = frozenset(str(status) for status in TERMINAL_JOB_STATUSES)
36
35
  _COMPLETED_STATUS = str(JobStatus.COMPLETED)
37
36
 
37
+ #: The tolerant-client check (`M55-org-runtime-policy`, issue #1537):
38
+ #: EVERY status this client can observe outside these two live values is
39
+ #: terminal, whether or not it is one this client's own ``JobStatus`` happens
40
+ #: to know by name. Membership is checked as "not one of the known LIVE
41
+ #: values", never "is one of the known TERMINAL values" — a closed terminal
42
+ #: set silently treats a FUTURE value it has never heard of as still-live
43
+ #: and polls forever (:class:`JobTimeoutError` only fires when the caller
44
+ #: passed a ``timeout``; with none, :meth:`JobHandle._get_cloud` hangs).
45
+ #: Mirrors the existing precedent for an unrecognized ``kind``
46
+ #: (``simulo.interfaces.platform.submit``): a value outside the known set is
47
+ #: treated as data to render, never as grounds to keep waiting on it.
48
+ _LIVE_STATUS_STRINGS = frozenset({str(JobStatus.QUEUED), str(JobStatus.RUNNING)})
49
+
38
50
 
39
51
  class JobFailedError(RuntimeError):
40
52
  """A cloud job reached a terminal ``failed``/``cancelled`` status.
@@ -154,7 +166,7 @@ class JobHandle:
154
166
  while True:
155
167
  record = client.get_job(str(self._job_id), scope=JOB_SCOPE_MINE)
156
168
  status = str(record.get("status"))
157
- if status in _TERMINAL_STATUS_STRINGS:
169
+ if status not in _LIVE_STATUS_STRINGS:
158
170
  if status == _COMPLETED_STATUS:
159
171
  return client.get_result(str(self._job_id), scope=JOB_SCOPE_MINE)
160
172
  exit_code = record.get("exit_code")
@@ -514,6 +514,26 @@ def _spawn_cloud(
514
514
  # already lives there, keyed by this same `job_name`.
515
515
  job_entry = manifest.get("jobs", {}).get(job_name) or {}
516
516
  system = (job_entry.get("resources") or {}).get("system")
517
+ # The job's own manifest-declared `timeout` (`M55-org-runtime-policy`,
518
+ # issue #1537) — already validated at `@app.job(timeout=...)` decoration
519
+ # time and recorded on THIS job's own manifest entry, exactly like
520
+ # `system` just above. Forwarded explicitly to `create_job` because the
521
+ # control plane never parses the uploaded manifest archive itself (see
522
+ # `JobSubmitRequest.manifest_timeout_s`'s own docstring).
523
+ manifest_timeout_s = job_entry.get("timeout")
524
+ if manifest_timeout_s is not None and (
525
+ isinstance(manifest_timeout_s, bool) or not isinstance(manifest_timeout_s, int)
526
+ ):
527
+ # This manifest was just written by THIS process (see the comment on
528
+ # `Manifest.from_json` re-parsing above) — a non-integer `timeout`
529
+ # here would mean `@app.job`'s own decoration-time validation
530
+ # somehow let one through, not an untrusted value to degrade
531
+ # silently on. Fail loudly rather than send a malformed wire field.
532
+ raise RuntimeError(
533
+ f"Package at {package_path} has a non-integer jobs[{job_name!r}].timeout "
534
+ f"({manifest_timeout_s!r}) in its manifest — this should be impossible; "
535
+ "please report this as a bug."
536
+ )
517
537
 
518
538
  tar_path = ensure_tar(package_path)
519
539
  archive_bytes = tar_path.read_bytes()
@@ -568,6 +588,7 @@ def _spawn_cloud(
568
588
  assets=asset_pins,
569
589
  app_name=app_name,
570
590
  system=system,
591
+ timeout_s=manifest_timeout_s,
571
592
  )
572
593
 
573
594
  raw_public_id = record.get("public_id")
@@ -590,9 +611,24 @@ def _spawn_cloud(
590
611
  # the ref resolved to the intended source job. Seed provenance still
591
612
  # carries machine UUIDs only, so keep this confirmation neutral rather
592
613
  # than leaking a UUID while that response contract catches up.
614
+ #
615
+ # `seed["name"]`/`seed["kind"]` are server-supplied text: a
616
+ # compromised control plane — or a user pointed at an attacker-run
617
+ # `SIMULO_API_URL` — could embed ANSI/OSC sequences here, so both
618
+ # route through the same module `cli.py`'s own sanitizers are built
619
+ # on (`preflight_render`) rather than a second one.
620
+ # `sanitize_server_text` alone is NOT enough here (cross-vendor
621
+ # review): it deliberately preserves TAB/LF for its usual
622
+ # multi-line preflight-finding callers, but this confirmation is a
623
+ # single line — an unstripped LF would forge an extra line this
624
+ # code never printed. `_single_line` collapses it instead.
625
+ from simulo._client.preflight_render import sanitize_server_text_single_line
626
+
593
627
  seed = record.get("seed") or {}
628
+ seed_name = sanitize_server_text_single_line(str(seed.get("name")))
629
+ seed_kind = sanitize_server_text_single_line(str(seed.get("kind")))
594
630
  print(
595
- f"Continuing from selected job ({seed.get('name')}, {seed.get('kind')} checkpoint)",
631
+ f"Continuing from selected job ({seed_name}, {seed_kind} checkpoint)",
596
632
  file=sys.stderr,
597
633
  )
598
634
 
@@ -296,6 +296,7 @@ class AssetApiClient:
296
296
  api_base_url=self._base_url,
297
297
  timeout=_ARCHIVE_DOWNLOAD_TIMEOUT_S,
298
298
  unavailable_hint=_UNAVAILABLE_HINT,
299
+ validate_download_url=True,
299
300
  )
300
301
  except http.HttpError as exc:
301
302
  raise _translate(exc) from exc
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
  from typing import TYPE_CHECKING, Any, Optional, Sequence
21
21
 
22
22
  from simulo._client.asset import Asset
23
- from simulo._client.packaging import canonical_args, package, read_bundle_package_id
23
+ from simulo._client.packaging import assert_credentials_outside_project, canonical_args, package, read_bundle_package_id
24
24
  from simulo._client.volume import Volume
25
25
  from simulo.interfaces.platform import is_platform_pinned
26
26
  from simulo.interfaces.platform.manifest import Manifest
@@ -222,6 +222,7 @@ def ensure_bundle(
222
222
  (deterministically restoring the correct content for this exact
223
223
  submission), never served as a silently wrong package.
224
224
  """
225
+ assert_credentials_outside_project(project_root)
225
226
  extra_asset_uris = tuple(sorted({asset.uri for asset in extra_assets}))
226
227
  cache_key = (id(app), job_name, canonical_args(args), extra_asset_uris)
227
228
  cached = _BUNDLE_CACHE.get(cache_key)