simulo 0.26.0__tar.gz → 0.27.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.27.0}/PKG-INFO +14 -1
  2. {simulo-0.26.0 → simulo-0.27.0}/PYPI.md +13 -0
  3. {simulo-0.26.0 → simulo-0.27.0}/pyproject.toml +1 -1
  4. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/cli.py +42 -8
  5. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/http.py +3 -1
  6. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/install_samples.py +138 -4
  7. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/preflight_api.py +4 -12
  8. {simulo-0.26.0 → simulo-0.27.0/src/simulo.egg-info}/PKG-INFO +14 -1
  9. {simulo-0.26.0 → simulo-0.27.0}/MANIFEST.in +0 -0
  10. {simulo-0.26.0 → simulo-0.27.0}/setup.cfg +0 -0
  11. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/__init__.py +0 -0
  12. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/__init__.py +0 -0
  13. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/_entrypoint.py +0 -0
  14. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/_mounts.py +0 -0
  15. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/_runner.py +0 -0
  16. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/_secure_downloads.py +0 -0
  17. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/app.py +0 -0
  18. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/asset.py +0 -0
  19. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/asset_api.py +0 -0
  20. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/asset_package.py +0 -0
  21. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/asset_pins.py +0 -0
  22. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/builtin_aliases.py +0 -0
  23. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/bundle.py +0 -0
  24. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/cancel_api.py +0 -0
  25. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/config.py +0 -0
  26. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/credentials.py +0 -0
  27. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/discovery.py +0 -0
  28. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/export_api.py +0 -0
  29. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/export_bundle.py +0 -0
  30. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/facades.py +0 -0
  31. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/identity_api.py +0 -0
  32. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/jobs_api.py +0 -0
  33. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/learning.py +0 -0
  34. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/login.py +0 -0
  35. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/mode.py +0 -0
  36. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/outputs.py +0 -0
  37. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/packaging.py +0 -0
  38. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/preflight_render.py +0 -0
  39. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/registry.py +0 -0
  40. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/runtime.py +0 -0
  41. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/runtime_display.py +0 -0
  42. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/seed_ref.py +0 -0
  43. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/stub.py +0 -0
  44. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/submit_api.py +0 -0
  45. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/__init__.py +0 -0
  46. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/inference/app.py.tmpl +0 -0
  47. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/inference/simuloignore.tmpl +0 -0
  48. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/scenario/app.py.tmpl +0 -0
  49. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/scenario/simuloignore.tmpl +0 -0
  50. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/training/app.py.tmpl +0 -0
  51. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  52. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/view_fragment.py +0 -0
  53. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/view_session_api.py +0 -0
  54. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/_client/volume.py +0 -0
  55. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/callbacks.py +0 -0
  56. {simulo-0.26.0 → simulo-0.27.0}/src/simulo/py.typed +0 -0
  57. {simulo-0.26.0 → simulo-0.27.0}/src/simulo.egg-info/SOURCES.txt +0 -0
  58. {simulo-0.26.0 → simulo-0.27.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  59. {simulo-0.26.0 → simulo-0.27.0}/src/simulo.egg-info/entry_points.txt +0 -0
  60. {simulo-0.26.0 → simulo-0.27.0}/src/simulo.egg-info/requires.txt +0 -0
  61. {simulo-0.26.0 → simulo-0.27.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.27.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
@@ -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.27.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"
@@ -131,6 +131,7 @@ import json
131
131
  import math
132
132
  import os
133
133
  import re
134
+ import shlex
134
135
  import shutil
135
136
  import socket
136
137
  import sys
@@ -194,7 +195,12 @@ from simulo._client.discovery import discover, import_app_module
194
195
  from simulo._client.export_api import DEFAULT_EXPORT_FORMAT, ExportApiClient, ExportApiError
195
196
  from simulo._client.export_bundle import PolicyBundleError, extract_policy_bundle
196
197
  from simulo._client.identity_api import IdentityApiClient, IdentityApiError
197
- from simulo._client.install_samples import InstallSamplesError, InstallSamplesUsageError, install_samples
198
+ from simulo._client.install_samples import (
199
+ DEFAULT_SAMPLES_DIRECTORY_NAME,
200
+ InstallSamplesError,
201
+ InstallSamplesUsageError,
202
+ install_samples,
203
+ )
198
204
  from simulo._client.jobs_api import JobsApiClient, JobsApiError, JobsApiHTTPError, JobsApiUnavailable
199
205
  from simulo._client.login import LoginError, perform_login
200
206
  from simulo._client.mode import DISCOVERY
@@ -8349,14 +8355,33 @@ def _build_parser() -> argparse.ArgumentParser:
8349
8355
  install_sub = install.add_subparsers(dest="install_command", required=True)
8350
8356
  samples = install_sub.add_parser(
8351
8357
  "samples",
8352
- help="Clone the sample collection into ./simulo-samples with full Git history.",
8358
+ help="Clone the sample collection into ./simulo-samples by default with full Git history.",
8359
+ )
8360
+ samples.add_argument(
8361
+ "--directory",
8362
+ metavar="PATH",
8363
+ type=Path,
8364
+ help="Install into PATH instead of ./simulo-samples. PATH's parent directory must already exist.",
8365
+ )
8366
+ samples.add_argument(
8367
+ "--ref",
8368
+ metavar="REF",
8369
+ help="Check out Git branch, tag, or commit REF after cloning.",
8353
8370
  )
8354
8371
 
8355
8372
  def _handle_install_samples(args: argparse.Namespace, extras: Sequence[str]) -> int:
8356
8373
  if extras:
8357
8374
  samples.error(f"unrecognized arguments: {' '.join(extras)}")
8375
+ directory = args.directory
8376
+ if directory is None:
8377
+ parent_directory = Path.cwd()
8378
+ directory_name = DEFAULT_SAMPLES_DIRECTORY_NAME
8379
+ else:
8380
+ target = directory if directory.is_absolute() else Path.cwd() / directory
8381
+ parent_directory = target.parent
8382
+ directory_name = target.name
8358
8383
  try:
8359
- installed = install_samples(Path.cwd())
8384
+ installed = install_samples(parent_directory, directory_name=directory_name, ref=args.ref)
8360
8385
  except InstallSamplesUsageError as exc:
8361
8386
  print(f"error: {exc}", file=sys.stderr)
8362
8387
  print("Nothing was installed.", file=sys.stderr)
@@ -8365,12 +8390,21 @@ def _build_parser() -> argparse.ArgumentParser:
8365
8390
  print(f"error: {exc}", file=sys.stderr)
8366
8391
  print("Nothing was installed.", file=sys.stderr)
8367
8392
  return 1
8368
- print(
8369
- f"Installed the Simulo samples in {installed.path} "
8370
- "(a normal Git clone: history, branches, and git pull all work)."
8371
- )
8393
+ if args.ref is None:
8394
+ print(
8395
+ f"Installed the Simulo samples in {installed.path} "
8396
+ "(a normal Git clone: history, branches, and git pull all work)."
8397
+ )
8398
+ else:
8399
+ print(
8400
+ f"Installed the Simulo samples in {installed.path} "
8401
+ f"(a normal Git clone pinned to {args.ref!r}; the checkout is detached)."
8402
+ )
8372
8403
  print("Next steps:")
8373
- print(" cd simulo-samples")
8404
+ if directory is None:
8405
+ print(" cd simulo-samples")
8406
+ else:
8407
+ print(f" cd {shlex.quote(str(installed.path))}")
8374
8408
  print(" cat README.md")
8375
8409
  print("README.md is the sample index; each sample folder has its own app.py to run with simulo run.")
8376
8410
  return 0
@@ -48,7 +48,7 @@ class HttpError(RuntimeError):
48
48
 
49
49
 
50
50
  class HttpUnavailable(HttpError):
51
- """The endpoint cannot be reached at all (nothing listening / DNS failure)."""
51
+ """The endpoint is unreachable or its response times out."""
52
52
 
53
53
 
54
54
  class HttpHTTPError(HttpError):
@@ -373,6 +373,8 @@ def request_bytes(
373
373
  raise _structured_http_error(exc) from exc
374
374
  except urllib.error.URLError as exc:
375
375
  raise HttpUnavailable(_unavailable_message(url, unavailable_hint, exc.reason)) from exc
376
+ except TimeoutError as exc:
377
+ raise HttpUnavailable(_unavailable_message(url, unavailable_hint, exc)) from exc
376
378
 
377
379
 
378
380
  def request_json(
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import re
6
7
  import shutil
7
8
  import subprocess
8
9
  import sys
@@ -134,6 +135,8 @@ def _remove_staging_directory(staging: Path, identity: tuple[int, int]) -> None:
134
135
  print(f"warning: partial samples install path no longer belongs to this invocation: {staging}", file=sys.stderr)
135
136
  return
136
137
  try:
138
+ if not os.path.islink(staging):
139
+ os.chmod(staging, 0o700)
137
140
  shutil.rmtree(staging, onerror=_remove_read_only_path)
138
141
  except OSError:
139
142
  print(f"warning: could not remove partial samples install at {staging}", file=sys.stderr)
@@ -160,6 +163,11 @@ def _target_exists_error(target: Path) -> InstallSamplesUsageError:
160
163
  return InstallSamplesUsageError(f"{target} already exists.")
161
164
 
162
165
 
166
+ def _path_exists(path: Path) -> bool:
167
+ """Return whether *path* exists, including a dangling symbolic link."""
168
+ return os.path.lexists(path)
169
+
170
+
163
171
  def _git_is_reachable_through_working_directory(
164
172
  named_git_path: Path, git_executable: Path, working_directory: Path
165
173
  ) -> bool:
@@ -190,11 +198,14 @@ def install_samples(
190
198
  *,
191
199
  repository_url: str = SAMPLES_REPOSITORY_URL,
192
200
  directory_name: str = DEFAULT_SAMPLES_DIRECTORY_NAME,
201
+ ref: str | None = None,
193
202
  timeout_s: int = CLONE_TIMEOUT_S,
194
203
  ) -> InstalledSamples:
195
204
  """Clone samples into *parent_directory* and reveal them atomically."""
196
205
  target = parent_directory / directory_name
197
- if target.exists():
206
+ if not parent_directory.is_dir():
207
+ raise InstallSamplesUsageError(f"Parent directory {parent_directory} does not exist or is not a directory.")
208
+ if _path_exists(target):
198
209
  raise _target_exists_error(target)
199
210
 
200
211
  git_path = shutil.which("git")
@@ -223,7 +234,10 @@ def install_samples(
223
234
  staging_identity = _directory_identity(staging)
224
235
 
225
236
  print("Cloning the samples with full history; this can take a minute.", file=sys.stderr)
226
- argv = [str(git_executable), "clone", "--", repository_url, str(staging)]
237
+ argv = [str(git_executable), "clone"]
238
+ if ref is not None:
239
+ argv.append("--no-checkout")
240
+ argv.extend(["--", repository_url, str(staging)])
227
241
  try:
228
242
  completed = _run_git(argv, timeout_s=timeout_s, env=_git_environment())
229
243
  except subprocess.TimeoutExpired as exc:
@@ -249,11 +263,131 @@ def install_samples(
249
263
  explain_public_repository=True,
250
264
  )
251
265
 
252
- if not _clone_has_sample_files(staging):
266
+ if ref is None and not _clone_has_sample_files(staging):
253
267
  _remove_staging_directory(staging, staging_identity)
254
268
  raise _clone_failure("Git clone produced an empty working tree.", completed.stderr)
255
269
 
256
- if target.exists():
270
+ if ref is not None:
271
+ try:
272
+ resolved = _run_git(
273
+ [
274
+ str(git_executable),
275
+ "-C",
276
+ str(staging),
277
+ "rev-parse",
278
+ "--verify",
279
+ "--end-of-options",
280
+ f"{ref}^{{commit}}",
281
+ ],
282
+ timeout_s=timeout_s,
283
+ env=_git_environment(),
284
+ )
285
+ except subprocess.TimeoutExpired as exc:
286
+ _remove_staging_directory(staging, staging_identity)
287
+ raise _clone_failure(f"Git could not resolve samples reference {ref!r}: timed out.", exc.stderr) from None
288
+ except KeyboardInterrupt:
289
+ _remove_staging_directory(staging, staging_identity)
290
+ raise
291
+ except OSError as exc:
292
+ _remove_staging_directory(staging, staging_identity)
293
+ reason = exc.strerror or str(exc)
294
+ raise InstallSamplesError(f"Could not resolve samples reference {ref!r}: {reason}.") from None
295
+ if resolved.returncode != 0:
296
+ try:
297
+ resolved = _run_git(
298
+ [
299
+ str(git_executable),
300
+ "-C",
301
+ str(staging),
302
+ "rev-parse",
303
+ "--verify",
304
+ "--end-of-options",
305
+ f"refs/remotes/origin/{ref}^{{commit}}",
306
+ ],
307
+ timeout_s=timeout_s,
308
+ env=_git_environment(),
309
+ )
310
+ except subprocess.TimeoutExpired as exc:
311
+ _remove_staging_directory(staging, staging_identity)
312
+ raise _clone_failure(
313
+ f"Git could not resolve samples reference {ref!r}: timed out.", exc.stderr
314
+ ) from None
315
+ except KeyboardInterrupt:
316
+ _remove_staging_directory(staging, staging_identity)
317
+ raise
318
+ except OSError as exc:
319
+ _remove_staging_directory(staging, staging_identity)
320
+ reason = exc.strerror or str(exc)
321
+ raise InstallSamplesError(f"Could not resolve samples reference {ref!r}: {reason}.") from None
322
+ if resolved.returncode != 0:
323
+ _remove_staging_directory(staging, staging_identity)
324
+ raise _clone_failure(f"Git could not resolve samples reference {ref!r}.", resolved.stderr)
325
+ commit = resolved.stdout.strip()
326
+ if re.fullmatch(r"[0-9a-fA-F]{40,64}", commit) is None:
327
+ _remove_staging_directory(staging, staging_identity)
328
+ raise InstallSamplesError(f"Git could not resolve samples reference {ref!r} to a commit.")
329
+
330
+ try:
331
+ checked_out = _run_git(
332
+ [str(git_executable), "-C", str(staging), "checkout", "--detach", commit],
333
+ timeout_s=timeout_s,
334
+ env=_git_environment(),
335
+ )
336
+ except subprocess.TimeoutExpired as exc:
337
+ _remove_staging_directory(staging, staging_identity)
338
+ raise _clone_failure(f"Git could not check out samples reference {ref!r}: timed out.", exc.stderr) from None
339
+ except KeyboardInterrupt:
340
+ _remove_staging_directory(staging, staging_identity)
341
+ raise
342
+ except OSError as exc:
343
+ _remove_staging_directory(staging, staging_identity)
344
+ reason = exc.strerror or str(exc)
345
+ raise InstallSamplesError(f"Could not check out samples reference {ref!r}: {reason}.") from None
346
+ if checked_out.returncode != 0:
347
+ _remove_staging_directory(staging, staging_identity)
348
+ raise _clone_failure(f"Git could not check out samples reference {ref!r}.", checked_out.stderr)
349
+ if not _clone_has_sample_files(staging):
350
+ _remove_staging_directory(staging, staging_identity)
351
+ raise _clone_failure("Git checkout produced an empty working tree.", checked_out.stderr)
352
+
353
+ try:
354
+ head = _run_git(
355
+ [str(git_executable), "-C", str(staging), "rev-parse", "--verify", "HEAD"],
356
+ timeout_s=timeout_s,
357
+ env=_git_environment(),
358
+ )
359
+ worktree = _run_git(
360
+ [
361
+ str(git_executable),
362
+ "-C",
363
+ str(staging),
364
+ "diff",
365
+ "--quiet",
366
+ commit,
367
+ "--",
368
+ ],
369
+ timeout_s=timeout_s,
370
+ env=_git_environment(),
371
+ )
372
+ except subprocess.TimeoutExpired as exc:
373
+ _remove_staging_directory(staging, staging_identity)
374
+ raise _clone_failure(f"Git could not verify samples reference {ref!r}: timed out.", exc.stderr) from None
375
+ except KeyboardInterrupt:
376
+ _remove_staging_directory(staging, staging_identity)
377
+ raise
378
+ except OSError as exc:
379
+ _remove_staging_directory(staging, staging_identity)
380
+ reason = exc.strerror or str(exc)
381
+ raise InstallSamplesError(f"Could not verify samples reference {ref!r}: {reason}.") from None
382
+ if head.returncode != 0 or worktree.returncode not in (0, 1):
383
+ _remove_staging_directory(staging, staging_identity)
384
+ stderr = head.stderr if head.returncode != 0 else worktree.stderr
385
+ raise _clone_failure(f"Git could not verify samples reference {ref!r}.", stderr)
386
+ if head.stdout.strip().lower() != commit.lower() or worktree.returncode != 0:
387
+ _remove_staging_directory(staging, staging_identity)
388
+ raise InstallSamplesError("Git checkout did not leave the requested samples revision clean.")
389
+
390
+ if _path_exists(target):
257
391
  _remove_staging_directory(staging, staging_identity)
258
392
  raise _target_exists_error(target)
259
393
 
@@ -17,18 +17,10 @@ always returns a :class:`PreflightOutcome`, either a genuine
17
17
  for the CLI to print verbatim before proceeding. Only a genuinely
18
18
  well-formed 200 response can carry a ``blocking`` finding.
19
19
 
20
- The final ``except Exception`` in :meth:`PreflightApiClient.evaluate` is
21
- deliberate, not sloppy: measured against this codebase's own ``http.py``,
22
- ``urllib.request.urlopen``'s connect/send-phase failures surface as
23
- ``urllib.error.URLError`` (wrapped here as the thin client's ``HttpUnavailable``),
24
- but a peer that accepts the connection and then simply never answers (this
25
- PR's own connection-timeout test reproduces it) times out INSIDE
26
- ``http.client.HTTPConnection.getresponse()`` — a call site ``http.py``'s
27
- existing ``except urllib.error.URLError`` does not wrap, so a bare
28
- ``socket.timeout``/``TimeoutError`` can reach this call site unwrapped. The
29
- broad catch is the safety net for exactly that gap (and for anything else
30
- unanticipated) — preflight must not add a second instance of this failure class to the
31
- submit path.
20
+ The shared HTTP helper wraps connection failures and response timeouts as
21
+ ``HttpUnavailable``. The final ``except Exception`` in
22
+ :meth:`PreflightApiClient.evaluate` preserves the fail-open contract for other
23
+ unexpected errors: only a well-formed findings response may block submission.
32
24
  """
33
25
 
34
26
  from __future__ import annotations
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo
3
- Version: 0.26.0
3
+ Version: 0.27.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
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes