simulo 0.8.0__tar.gz → 0.10.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 (39) hide show
  1. {simulo-0.8.0/src/simulo.egg-info → simulo-0.10.0}/PKG-INFO +2 -2
  2. {simulo-0.8.0 → simulo-0.10.0}/pyproject.toml +2 -2
  3. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/app.py +40 -4
  4. simulo-0.10.0/src/simulo/_client/cancel_api.py +105 -0
  5. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/cli.py +835 -25
  6. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/http.py +10 -2
  7. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/jobs_api.py +21 -4
  8. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/submit_api.py +14 -2
  9. simulo-0.10.0/src/simulo/_client/view_session_api.py +122 -0
  10. {simulo-0.8.0 → simulo-0.10.0/src/simulo.egg-info}/PKG-INFO +2 -2
  11. {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/SOURCES.txt +2 -0
  12. {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/requires.txt +1 -1
  13. {simulo-0.8.0 → simulo-0.10.0}/MANIFEST.in +0 -0
  14. {simulo-0.8.0 → simulo-0.10.0}/PYPI.md +0 -0
  15. {simulo-0.8.0 → simulo-0.10.0}/setup.cfg +0 -0
  16. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/__init__.py +0 -0
  17. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/__init__.py +0 -0
  18. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_entrypoint.py +0 -0
  19. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_mounts.py +0 -0
  20. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_runner.py +0 -0
  21. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/asset.py +0 -0
  22. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/bundle.py +0 -0
  23. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/config.py +0 -0
  24. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/credentials.py +0 -0
  25. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/discovery.py +0 -0
  26. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/learning.py +0 -0
  27. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/login.py +0 -0
  28. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/manifest.py +0 -0
  29. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/mode.py +0 -0
  30. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/packaging.py +0 -0
  31. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/registry.py +0 -0
  32. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/runtime.py +0 -0
  33. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/stub.py +0 -0
  34. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/volume.py +0 -0
  35. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/callbacks.py +0 -0
  36. {simulo-0.8.0 → simulo-0.10.0}/src/simulo/py.typed +0 -0
  37. {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  38. {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/entry_points.txt +0 -0
  39. {simulo-0.8.0 → simulo-0.10.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.8.0
3
+ Version: 0.10.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.2,>=0.1
18
+ Requires-Dist: simulo-interfaces<0.4,>=0.3
19
19
  Requires-Dist: mcap<2,>=1.3
20
20
  Provides-Extra: dev
21
21
  Requires-Dist: black>=24.0; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo"
7
- version = "0.8.0"
7
+ version = "0.10.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,7 +25,7 @@ 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 = ["simulo-interfaces>=0.1,<0.2", "mcap>=1.3,<2"]
28
+ dependencies = ["simulo-interfaces>=0.3,<0.4", "mcap>=1.3,<2"]
29
29
 
30
30
  [project.optional-dependencies]
31
31
  dev = ["black>=24.0", "isort>=5.13", "ruff>=0.5", "mypy>=1.10", "pytest>=8.2", "build>=1.2", "twine>=5.1"]
@@ -70,6 +70,33 @@ def spawn_recorder() -> Iterator[list["JobHandle"]]:
70
70
  _SPAWN_RECORDER = previous
71
71
 
72
72
 
73
+ # ---------------------------------------------------------------------------
74
+ # Viewstream ambient signal — `simulo run --viewstream` scopes this for the
75
+ # duration of one submit so a cloud `spawn()` deep inside the user's
76
+ # @app.local_entrypoint (which `simulo run` does not call directly) can
77
+ # attach it to the job-create call. Deliberately NOT a `spawn()`/`submit()`
78
+ # kwarg: those kwargs feed the job's `args`, whose canonical form is the
79
+ # `package_id` sha256 — a streaming flag must never change the package hash
80
+ # (the live-visualization plan's wire-contract rule). Mirrors the
81
+ # `spawn_recorder` pattern above for the same "can't thread a return value
82
+ # through arbitrary user code" reason.
83
+ # ---------------------------------------------------------------------------
84
+
85
+ _VIEWSTREAM_REQUESTED = False
86
+
87
+
88
+ @contextlib.contextmanager
89
+ def viewstream_requested(enabled: bool) -> Iterator[None]:
90
+ """Scope ``--viewstream`` for one ``simulo run`` submit call."""
91
+ global _VIEWSTREAM_REQUESTED
92
+ previous = _VIEWSTREAM_REQUESTED
93
+ _VIEWSTREAM_REQUESTED = enabled
94
+ try:
95
+ yield
96
+ finally:
97
+ _VIEWSTREAM_REQUESTED = previous
98
+
99
+
73
100
  @dataclass
74
101
  class JobSpec:
75
102
  """Recorded metadata for one ``@app.job``-decorated function."""
@@ -125,8 +152,17 @@ def _spawn_local(job_name: str, package_path: Path) -> JobHandle:
125
152
  return JobHandle(job_id, package_path, job_name)
126
153
 
127
154
 
128
- def _spawn_cloud(job_name: str, package_path: Path, creds: Optional[dict[str, Any]]) -> JobHandle:
129
- """Tar the bundle, upload it, create the job, and return a cloud JobHandle."""
155
+ def _spawn_cloud(
156
+ job_name: str, package_path: Path, creds: Optional[dict[str, Any]], *, viewstream: bool = False
157
+ ) -> JobHandle:
158
+ """Tar the bundle, upload it, create the job, and return a cloud JobHandle.
159
+
160
+ ``viewstream`` rides ONLY the job-create call, as an optional top-level
161
+ body field (never inside ``args``, which feeds the ``package_id`` sha256
162
+ — see the live-visualization plan's wire-contract rule): a package's
163
+ content-addressed identity must never change based on whether this
164
+ particular execution asked for a live stream.
165
+ """
130
166
  from simulo._client.packaging import MANIFEST_FILENAME, ensure_tar
131
167
  from simulo._client.submit_api import SubmitApiClient
132
168
 
@@ -151,7 +187,7 @@ def _spawn_cloud(job_name: str, package_path: Path, creds: Optional[dict[str, An
151
187
  archive_size=len(archive_bytes),
152
188
  )
153
189
  client.upload_archive(package_id, archive_bytes)
154
- record = client.create_job(package_id=package_id, job_name=job_name, args=args)
190
+ record = client.create_job(package_id=package_id, job_name=job_name, args=args, viewstream=viewstream)
155
191
 
156
192
  return JobHandle(JobId(str(record["job_id"])), package_path, job_name, cloud=True, base_url=base_url, token=token)
157
193
 
@@ -210,7 +246,7 @@ class JobFunction:
210
246
  else:
211
247
  creds = credentials.try_get_valid_credentials()
212
248
  if _cloud_mode_active(creds):
213
- handle = _spawn_cloud(self._spec.name, package_path, creds)
249
+ handle = _spawn_cloud(self._spec.name, package_path, creds, viewstream=_VIEWSTREAM_REQUESTED)
214
250
  else:
215
251
  handle = _spawn_local(self._spec.name, package_path)
216
252
  _record_spawn(handle)
@@ -0,0 +1,105 @@
1
+ """HTTP client for the job cancel endpoint (stdlib only, torch-free).
2
+
3
+ Wire contract (job-cancel-and-demo-apps plan — see
4
+ ``context/implementation-plans/job-cancel-and-demo-apps-plan.md``; the control
5
+ plane's half of this route is built by a parallel PR, so this client codes
6
+ directly against the plan's PINNED contract rather than waiting on it):
7
+ ``POST /v1/jobs/{job_id}/cancel`` (Cognito bearer, org-scoped).
8
+
9
+ * 200 ``{"job_id": ..., "status": "cancelled"}`` — a QUEUED job is cancelled
10
+ immediately (it can never be claimed by a worker after this).
11
+ * 202 ``{"job_id": ..., "status": "running", "cancel_requested": true}`` — a
12
+ RUNNING job has cancellation requested; the worker confirms termination
13
+ within one heartbeat interval. Idempotent — a repeat cancel of an
14
+ already-cancel-requested running job returns the same 202 shape again.
15
+ * 409 ``job_not_running`` — the job is already in a terminal state
16
+ (completed/failed/cancelled); the response message carries the actual
17
+ status, but the caller (``simulo._client.cli``) does not surface it
18
+ verbatim — it reuses the same friendly "has ended" copy ``simulo view``
19
+ uses for the identical situation.
20
+ * 404 ``job_not_found`` — unknown or cross-org job id (anti-enumeration;
21
+ never 403). This is the cancel route's ONLY pinned 404 code — mirrors
22
+ ``view_session_api.py``'s contract, and the caller relies on that the same
23
+ way: any OTHER 404 code means the ``/cancel`` route itself isn't deployed
24
+ in this environment (e.g. prod before this feature ships there), not that
25
+ the job is missing — see ``cli._CANCEL_UNAVAILABLE_MESSAGE``.
26
+
27
+ Error responses use the standard ``{"error": {"code", "message", "request_id"}}``
28
+ envelope, parsed by ``http.py`` exactly like every other client in this
29
+ package.
30
+
31
+ The route path is a LOCAL constant here rather than imported from
32
+ ``simulo.interfaces.platform.runs`` — deliberately, the same reconciliation
33
+ call ``view_session_api.py`` made: the control-plane cancel route is being
34
+ built by a parallel PR and may land independently of this one, so this client
35
+ codes directly against the plan's pinned path/response shape rather than
36
+ sharing a file with that PR (avoiding a merge conflict). Reconcile into a
37
+ shared ``simulo.interfaces.platform`` constant in a follow-up once that PR has
38
+ merged, if a shared module materializes for it.
39
+
40
+ Shares request plumbing (structured-error parsing, the https-when-token
41
+ guard, the foreign-host bearer rule) with ``jobs_api.py``/``view_session_api.py``
42
+ via ``http.py`` — one request path, not reimplemented a fourth time.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ from typing import Any, Optional
48
+
49
+ from simulo._client import http
50
+
51
+ #: ``POST`` — request cancellation of a queued or running job. ``.format(job_id=...)``.
52
+ CANCEL_ROUTE_TEMPLATE = "/v1/jobs/{job_id}/cancel"
53
+
54
+ _REQUEST_TIMEOUT_S = 10.0 # every outbound call has an explicit timeout (NFR)
55
+ _UNAVAILABLE_HINT = "Check SIMULO_API_URL / SIMULO_ENV, and that you are logged in (`simulo login`)."
56
+
57
+ #: Required fields of a well-formed cancel response (see module docstring).
58
+ _REQUIRED_FIELDS = ("job_id", "status")
59
+
60
+ # Backward-compatible-style aliases — same convention as jobs_api.py /
61
+ # view_session_api.py: these are literally ``http.HttpError``/``http.HttpHTTPError``
62
+ # (not new subclasses), so callers that already catch ``JobsApiError`` (also
63
+ # ``http.HttpError``) catch these too without any extra wiring.
64
+ CancelApiError = http.HttpError
65
+ CancelApiHTTPError = http.HttpHTTPError
66
+
67
+
68
+ class CancelApiClient:
69
+ """Client for ``POST /v1/jobs/{job_id}/cancel``."""
70
+
71
+ def __init__(self, base_url: str, *, token: Optional[str] = None) -> None:
72
+ if not base_url.startswith(("http://", "https://")):
73
+ raise CancelApiError(f"API base URL must be an http(s) URL, got {base_url!r}.")
74
+ self._base_url = base_url.rstrip("/")
75
+ self._token = token
76
+
77
+ @property
78
+ def base_url(self) -> str:
79
+ return self._base_url
80
+
81
+ def cancel_job(self, job_id: str) -> dict[str, Any]:
82
+ """Request cancellation of *job_id*; returns the parsed 200/202 body.
83
+
84
+ Raises :class:`CancelApiHTTPError` on any non-2xx response — the
85
+ caller (``simulo._client.cli``) classifies by ``exc.code``, never by
86
+ ``exc.status`` alone (both ``job_not_running`` and the route-missing
87
+ degradation share statuses with other codes, exactly like
88
+ ``ViewSessionApiClient.create_view_session``'s callers already do).
89
+ """
90
+ path = CANCEL_ROUTE_TEMPLATE.format(job_id=http.quote_path_segment(job_id))
91
+ payload = http.request_json(
92
+ "POST",
93
+ self._base_url + path,
94
+ token=self._token,
95
+ api_base_url=self._base_url,
96
+ json_body={},
97
+ timeout=_REQUEST_TIMEOUT_S,
98
+ unavailable_hint=_UNAVAILABLE_HINT,
99
+ )
100
+ if not isinstance(payload, dict):
101
+ raise CancelApiError(f"Malformed cancel response for job {job_id!r}: expected a JSON object.")
102
+ missing = [field for field in _REQUIRED_FIELDS if field not in payload]
103
+ if missing:
104
+ raise CancelApiError(f"Malformed cancel response for job {job_id!r}: missing field(s) {missing}.")
105
+ return payload