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.
- {simulo-0.8.0/src/simulo.egg-info → simulo-0.10.0}/PKG-INFO +2 -2
- {simulo-0.8.0 → simulo-0.10.0}/pyproject.toml +2 -2
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/app.py +40 -4
- simulo-0.10.0/src/simulo/_client/cancel_api.py +105 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/cli.py +835 -25
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/http.py +10 -2
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/jobs_api.py +21 -4
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/submit_api.py +14 -2
- simulo-0.10.0/src/simulo/_client/view_session_api.py +122 -0
- {simulo-0.8.0 → simulo-0.10.0/src/simulo.egg-info}/PKG-INFO +2 -2
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/SOURCES.txt +2 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/requires.txt +1 -1
- {simulo-0.8.0 → simulo-0.10.0}/MANIFEST.in +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/PYPI.md +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/setup.cfg +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/__init__.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/__init__.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_entrypoint.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_mounts.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/_runner.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/asset.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/bundle.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/config.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/credentials.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/discovery.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/learning.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/login.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/manifest.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/mode.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/packaging.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/registry.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/runtime.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/stub.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/_client/volume.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/callbacks.py +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo/py.typed +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/dependency_links.txt +0 -0
- {simulo-0.8.0 → simulo-0.10.0}/src/simulo.egg-info/entry_points.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
129
|
-
|
|
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
|