simulo 0.16.0__tar.gz → 0.17.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 (56) hide show
  1. {simulo-0.16.0/src/simulo.egg-info → simulo-0.17.0}/PKG-INFO +7 -6
  2. {simulo-0.16.0 → simulo-0.17.0}/pyproject.toml +34 -3
  3. simulo-0.17.0/src/simulo/__init__.py +409 -0
  4. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/asset.py +6 -5
  5. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/asset_api.py +2 -2
  6. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/asset_package.py +20 -4
  7. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/asset_pins.py +3 -3
  8. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/cli.py +449 -50
  9. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/facades.py +144 -18
  10. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/jobs_api.py +14 -4
  11. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/learning.py +110 -12
  12. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/scenario/app.py.tmpl +7 -3
  13. {simulo-0.16.0 → simulo-0.17.0/src/simulo.egg-info}/PKG-INFO +7 -6
  14. {simulo-0.16.0 → simulo-0.17.0}/src/simulo.egg-info/requires.txt +6 -5
  15. simulo-0.16.0/src/simulo/__init__.py +0 -188
  16. {simulo-0.16.0 → simulo-0.17.0}/MANIFEST.in +0 -0
  17. {simulo-0.16.0 → simulo-0.17.0}/PYPI.md +0 -0
  18. {simulo-0.16.0 → simulo-0.17.0}/setup.cfg +0 -0
  19. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/__init__.py +0 -0
  20. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/_entrypoint.py +0 -0
  21. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/_mounts.py +0 -0
  22. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/_runner.py +0 -0
  23. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/app.py +0 -0
  24. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/artifacts.py +0 -0
  25. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/builtin_aliases.py +0 -0
  26. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/bundle.py +0 -0
  27. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/cancel_api.py +0 -0
  28. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/config.py +0 -0
  29. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/credentials.py +0 -0
  30. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/discovery.py +0 -0
  31. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/http.py +0 -0
  32. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/login.py +0 -0
  33. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/mode.py +0 -0
  34. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/packaging.py +0 -0
  35. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/preflight_api.py +0 -0
  36. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/preflight_render.py +0 -0
  37. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/registry.py +0 -0
  38. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/runtime.py +0 -0
  39. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/runtime_display.py +0 -0
  40. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/seed_ref.py +0 -0
  41. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/stub.py +0 -0
  42. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/submit_api.py +0 -0
  43. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/__init__.py +0 -0
  44. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/inference/app.py.tmpl +0 -0
  45. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/inference/simuloignore.tmpl +0 -0
  46. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/scenario/simuloignore.tmpl +0 -0
  47. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/training/app.py.tmpl +0 -0
  48. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/templates/training/simuloignore.tmpl +0 -0
  49. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/view_session_api.py +0 -0
  50. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/_client/volume.py +0 -0
  51. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/callbacks.py +0 -0
  52. {simulo-0.16.0 → simulo-0.17.0}/src/simulo/py.typed +0 -0
  53. {simulo-0.16.0 → simulo-0.17.0}/src/simulo.egg-info/SOURCES.txt +0 -0
  54. {simulo-0.16.0 → simulo-0.17.0}/src/simulo.egg-info/dependency_links.txt +0 -0
  55. {simulo-0.16.0 → simulo-0.17.0}/src/simulo.egg-info/entry_points.txt +0 -0
  56. {simulo-0.16.0 → simulo-0.17.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.16.0
3
+ Version: 0.17.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,17 +15,18 @@ 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.11,>=0.10
18
+ Requires-Dist: simulo-interfaces<0.12,>=0.11
19
19
  Requires-Dist: mcap<2,>=1.3
20
20
  Requires-Dist: defusedxml>=0.7.1
21
21
  Requires-Dist: usd-core<27,>=25.5
22
22
  Provides-Extra: assets
23
23
  Requires-Dist: usd-core<27,>=25.5; extra == "assets"
24
24
  Provides-Extra: dev
25
- Requires-Dist: black>=24.0; extra == "dev"
26
- Requires-Dist: isort>=5.13; extra == "dev"
27
- Requires-Dist: ruff>=0.5; extra == "dev"
28
- Requires-Dist: mypy>=1.10; extra == "dev"
25
+ Requires-Dist: black==26.5.1; extra == "dev"
26
+ Requires-Dist: isort==8.0.1; extra == "dev"
27
+ Requires-Dist: ruff==0.15.21; extra == "dev"
28
+ Requires-Dist: mypy==2.3.0; extra == "dev"
29
+ Requires-Dist: types-defusedxml==0.7.0.20260504; extra == "dev"
29
30
  Requires-Dist: pytest>=8.2; extra == "dev"
30
31
  Requires-Dist: build>=1.2; extra == "dev"
31
32
  Requires-Dist: twine>=5.1; extra == "dev"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo"
7
- version = "0.16.0"
7
+ version = "0.17.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.10,<0.11",
29
+ "simulo-interfaces>=0.11,<0.12",
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
@@ -54,7 +54,27 @@ dependencies = [
54
54
  # already installed unconditionally) but harmless — resolvers don't warn on
55
55
  # an extra whose contents are already satisfied by the base requirement.
56
56
  assets = ["usd-core>=25.5,<27"]
57
- dev = ["black>=24.0", "isort>=5.13", "ruff>=0.5", "mypy>=1.10", "pytest>=8.2", "build>=1.2", "twine>=5.1"]
57
+ # black/isort/ruff/mypy pinned EXACT, not >=: see simulo-backend/pyproject.toml's
58
+ # `dev` extra for the full rationale (2026-08-01 ruff 0.15.21->0.16.1 CI incident).
59
+ # Keep in lockstep with `.github/workflows/simulo-ci.yml`'s `ruff`/`black`/`isort`/`mypy`
60
+ # steps, which resolve their versions from THIS extra (`pip install -e ".[dev]"`).
61
+ #
62
+ # `types-defusedxml` is pinned EXACT for the same reason as the four tools above:
63
+ # a stub package is a rule set, and a new typeshed release can tighten a
64
+ # signature and turn a green tree red with no code change. `defusedxml` is a
65
+ # declared RUNTIME dependency of this package (asset_package.py parses URDF with
66
+ # it), so without the stubs mypy reported `Library stubs not installed` and —
67
+ # worse — silently skipped type-checking every call into it.
68
+ dev = [
69
+ "black==26.5.1",
70
+ "isort==8.0.1",
71
+ "ruff==0.15.21",
72
+ "mypy==2.3.0",
73
+ "types-defusedxml==0.7.0.20260504",
74
+ "pytest>=8.2",
75
+ "build>=1.2",
76
+ "twine>=5.1",
77
+ ]
58
78
  # `dev` carries build/twine but deliberately not commitizen; `release` below
59
79
  # adds commitizen for cutting version bumps without a second build/twine copy.
60
80
  release = ["commitizen>=3.27", "build>=1.2", "twine>=5.1"]
@@ -91,6 +111,17 @@ namespace_packages = true
91
111
  explicit_package_bases = true
92
112
  strict = true
93
113
 
114
+ # `pxr` (the USD Python bindings, shipped by `usd-core`) is a C++-backed
115
+ # extension module that publishes neither type stubs nor a `py.typed` marker,
116
+ # so there is no more precise type available for it than the module object
117
+ # itself. Suppressed at the module level — the same mechanism, and for the same
118
+ # reason, as simulo-backend/pyproject.toml's skrl / onnxruntime overrides.
119
+ # Scoped to `pxr` alone, so it cannot hide a real type error anywhere else:
120
+ # `_load_pxr_usdutils` is the ONLY place this package imports it.
121
+ [[tool.mypy.overrides]]
122
+ module = ["pxr", "pxr.*"]
123
+ ignore_missing_imports = true
124
+
94
125
  [tool.pytest.ini_options]
95
126
  testpaths = ["tests"]
96
127
  addopts = "--import-mode=importlib"
@@ -0,0 +1,409 @@
1
+ """Simulo thin client — the user-facing, torch-free SDK surface.
2
+
3
+ This is the ONE Simulo distribution that ships ``simulo/__init__.py``. The
4
+ ``extend_path`` call below unions every ``simulo/`` directory on ``sys.path`` into
5
+ this package's ``__path__`` so the PEP 420 namespace portions shipped by sibling
6
+ distributions (``simulo.core`` / ``simulo.backend`` / ``simulo.recording`` from
7
+ ``simulo-backend``; ``simulo.interfaces`` from ``simulo-interfaces``) resolve
8
+ alongside the clean top-level ``simulo.App`` re-exported here.
9
+
10
+ It MUST run before the re-exports below, because importing the thin client's
11
+ internal ``App`` module pulls in ``simulo.interfaces`` (a sibling distribution).
12
+
13
+ **The one-import rule.** Everything a user can legitimately need is reachable
14
+ from ``import simulo``. ``simulo.interfaces`` is a packaging detail two
15
+ distributions share so the thin client and the backend hold ONE definition
16
+ instead of drifting copies — it is never a namespace a user types. A user who
17
+ can write ``simulo.Physics.rigid(...)`` must not have to discover a second
18
+ import to write ``except simulo.JobFailedError`` or
19
+ ``collision=simulo.Collision.enabled()``.
20
+
21
+ The rule is enforced as a rule, not as a hand-kept list: the thin client's
22
+ one-import-rule test derives the expected surface from
23
+ ``simulo.interfaces.__all__`` and ``simulo.interfaces.authoring.__all__`` and
24
+ fails on any name that is not reachable here or explicitly declared in that
25
+ test's exclusions table with a reason. A hand-maintained whitelist is only ever
26
+ as current as the last person who remembered to edit it, and two names were
27
+ missed that way before the rule replaced it.
28
+
29
+ Three kinds of name live on ``simulo``:
30
+
31
+ * **Platform** names — ``App / Runtime / Volume / Asset / Artifact /
32
+ ArtifactKind / callbacks`` — are **real, eager** attributes bound below.
33
+ They are always torch-free (``Artifact`` is the ``simulo-interfaces``
34
+ contract dataclass, ``ArtifactKind`` its wire-stable ``StrEnum``, both
35
+ re-exported).
36
+ * **Contract vocabulary** — the shared error hierarchy (``SimuloError`` and its
37
+ subclasses), the ``NewType`` identifier aliases (``JobId``, ``ProjectId``, …),
38
+ and the authoring value types that are not already carried by the lazy
39
+ resolver (``Collision``, ``Transform``, the primitive shapes, the factory
40
+ return types, the markers, …) — are also **real, eager** attributes. They cost
41
+ nothing to bind: ``simulo.interfaces.authoring`` is a zero-dependency package
42
+ that the thin client's own learning resolver already imports at module scope,
43
+ so this adds no import and no dependency. Errors and IDs are likewise
44
+ stdlib-only (``NewType`` is the underlying ``str`` at runtime).
45
+ * **Lazy, mode-aware** names — the learning names ``Task / Scene / Robot /
46
+ LearningEnv / RLTrainer / …`` plus the execution-time ``save_artifact`` and
47
+ ``run`` (the scenario execution loop) — are
48
+ resolved through the PEP 562 ``__getattr__`` below, which delegates to the
49
+ thin client's internal learning-name resolver. At submit (discovery mode) they resolve to
50
+ lean stand-ins so the user app imports without the heavy runtime; only the
51
+ backend runner (execution mode) resolves them to the real implementations.
52
+ Because the platform names are real attributes, ``__getattr__`` fires *only*
53
+ for the lazy names. The RELOCATED authoring value types (``Pose`` /
54
+ ``Material`` / ``Physics`` / ``Light`` / ``Terrain`` / ``World`` /
55
+ ``Entity`` / ``Visual`` / ``RecordConfig``) ride the same resolver but
56
+ resolve to their real, dependency-free ``simulo.interfaces.authoring``
57
+ class in BOTH modes — they need no heavy runtime, and exactly one
58
+ definition exists. (``Terrain`` is ground you generate; ``World`` is a
59
+ prebuilt place you bring, ``World.usd(...)``.)
60
+ """
61
+
62
+ import pkgutil as _pkgutil
63
+ from typing import TYPE_CHECKING
64
+
65
+ __path__ = _pkgutil.extend_path(__path__, __name__)
66
+
67
+ from simulo import callbacks
68
+ from simulo._client.app import App
69
+ from simulo._client.asset import Asset
70
+ from simulo._client.learning import LEARNING_NAMES as _LEARNING_NAMES
71
+ from simulo._client.learning import resolve as _resolve_learning
72
+ from simulo._client.runtime import Runtime
73
+ from simulo._client.volume import Volume
74
+
75
+ # --- Contract vocabulary, re-exported under the one-import rule ---------------
76
+ #
77
+ # The shared error hierarchy and the identifier aliases. Both are stdlib-only:
78
+ # the exceptions derive from ``Exception``, and every identifier is a
79
+ # ``typing.NewType`` over ``str`` (zero runtime cost, distinct to a type
80
+ # checker). A user catching a failed job writes ``except simulo.JobFailedError``,
81
+ # never a second import.
82
+ from simulo.interfaces import (
83
+ CheckpointId,
84
+ ContractViolationError,
85
+ Digest,
86
+ ImportResolutionError,
87
+ JobFailedError,
88
+ JobId,
89
+ OrganizationId,
90
+ PackageId,
91
+ ProjectId,
92
+ ResourceId,
93
+ ResourceNotFoundError,
94
+ ResourceUri,
95
+ ResumeIncompatibleError,
96
+ SimuloError,
97
+ TagId,
98
+ TrainedModelId,
99
+ )
100
+
101
+ # The authoring value types that the lazy resolver does NOT already carry.
102
+ # Every one is a real, dependency-free class with exactly one definition, so —
103
+ # unlike the learning names — there is nothing mode-aware about them and no
104
+ # reason to defer: binding them eagerly is what makes ``simulo.Collision`` and
105
+ # ``simulo.Transform`` exist at all.
106
+ #
107
+ # ``simulo.Asset`` (the thin client's registry HANDLE, imported above — the
108
+ # name every shipped example and doc page uses) and
109
+ # ``simulo.interfaces.authoring.Asset`` (the model-source spec) used to share
110
+ # one name — two different user-facing
111
+ # classes, both exposing ``.usd()`` / ``.urdf()``, so re-exporting the
112
+ # authoring one here would have silently shadowed the handle users actually
113
+ # want. #693 (owner decision) resolved the collision by renaming the
114
+ # contract-side class to ``AssetSource``; with the names distinct, it is
115
+ # re-exported here like every other concrete authoring type — no exclusion
116
+ # needed any more.
117
+ from simulo.interfaces.authoring import (
118
+ ArrowMarker,
119
+ AssetEntity,
120
+ AssetSource,
121
+ AxesMarker,
122
+ Collision,
123
+ Cone,
124
+ Cuboid,
125
+ Cylinder,
126
+ DeformableMaterial,
127
+ DeformablePhysics,
128
+ DistantLight,
129
+ DomeLight,
130
+ GroundPlane,
131
+ LightConfig,
132
+ PointMarker,
133
+ PrimitiveShape,
134
+ RigidPhysics,
135
+ RoughTerrain,
136
+ Sphere,
137
+ SphereMarker,
138
+ SubTerrainConfig,
139
+ SurfaceMaterial,
140
+ TerrainConfig,
141
+ Transform,
142
+ URDFAsset,
143
+ USDAsset,
144
+ USDWorld,
145
+ VisualMarkerBase,
146
+ )
147
+ from simulo.interfaces.platform import Artifact, ArtifactKind
148
+
149
+ # Public surface = eager platform exports (real attributes; ``__getattr__`` never
150
+ # fires for these) + the lazily-resolved learning names. Kept a static literal so
151
+ # tooling recognises the re-exports; a test guards it against ``LEARNING_NAMES`` drift.
152
+ __all__ = [
153
+ # platform (eager, real)
154
+ "App",
155
+ "Artifact",
156
+ "ArtifactKind",
157
+ "Runtime",
158
+ "Volume",
159
+ "Asset",
160
+ "callbacks",
161
+ # contract errors (eager, real) — the shared exception hierarchy
162
+ "SimuloError",
163
+ "ContractViolationError",
164
+ "ImportResolutionError",
165
+ "ResourceNotFoundError",
166
+ "ResumeIncompatibleError",
167
+ "JobFailedError",
168
+ # contract identifiers (eager, real) — NewType aliases over ``str``
169
+ "OrganizationId",
170
+ "ProjectId",
171
+ "PackageId",
172
+ "JobId",
173
+ "ResourceId",
174
+ "CheckpointId",
175
+ "TrainedModelId",
176
+ "TagId",
177
+ "ResourceUri",
178
+ "Digest",
179
+ # authoring value types (eager, real) — the rest of the vocabulary the
180
+ # lazy resolver below does not carry
181
+ "Collision",
182
+ "RigidPhysics",
183
+ "DeformableMaterial",
184
+ "DeformablePhysics",
185
+ "SurfaceMaterial",
186
+ "AssetSource",
187
+ "USDAsset",
188
+ "URDFAsset",
189
+ "LightConfig",
190
+ "DistantLight",
191
+ "DomeLight",
192
+ "TerrainConfig",
193
+ "GroundPlane",
194
+ "SubTerrainConfig",
195
+ "RoughTerrain",
196
+ "USDWorld",
197
+ "PrimitiveShape",
198
+ "Cone",
199
+ "Cuboid",
200
+ "Sphere",
201
+ "Cylinder",
202
+ "Transform",
203
+ "AssetEntity",
204
+ "VisualMarkerBase",
205
+ "AxesMarker",
206
+ "SphereMarker",
207
+ "ArrowMarker",
208
+ "PointMarker",
209
+ # lazy, mode-aware (the learning names + save_artifact)
210
+ "ActuatorGainsConfig",
211
+ "Camera",
212
+ "CameraSpawnConfig",
213
+ "ContactSensor",
214
+ "DifferentialIKController",
215
+ "Entity",
216
+ "ForceTorqueSensor",
217
+ "FrameTransformer",
218
+ "FrameTransformerTarget",
219
+ "GripperState",
220
+ "IMU",
221
+ "LearningEnv",
222
+ "Light",
223
+ "Material",
224
+ "ObservationComponent",
225
+ "ObservationSet",
226
+ "OperationalSpaceController",
227
+ "Physics",
228
+ "Player",
229
+ "Policy",
230
+ "Pose",
231
+ "RLPlayer",
232
+ "RLTrainer",
233
+ "RayCaster",
234
+ "RayPattern",
235
+ "RecordConfig",
236
+ "RewardComponent",
237
+ "RewardSet",
238
+ "Robot",
239
+ "Scenario",
240
+ "Scene",
241
+ "SensorOffset",
242
+ "Simulation",
243
+ "SurfaceGripperActuator",
244
+ "Task",
245
+ "Terrain",
246
+ "TerminationCondition",
247
+ "TerminationSet",
248
+ "Trainer",
249
+ "Visual",
250
+ "World",
251
+ "run",
252
+ "save_artifact",
253
+ ]
254
+
255
+
256
+ def __getattr__(name: str) -> object:
257
+ """PEP 562 hook: lazily resolve the mode-aware learning names.
258
+
259
+ Fires only for attributes missing from the module dict — i.e. never for the
260
+ real platform exports above. Delegates the learning names to the thin
261
+ client's internal resolver (discovery stand-in vs. real
262
+ ``simulo.core`` class, by mode); everything else is a genuine ``AttributeError``.
263
+ """
264
+ if name in _LEARNING_NAMES:
265
+ return _resolve_learning(name)
266
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
267
+
268
+
269
+ def __dir__() -> list[str]:
270
+ """Expose only the public surface to ``dir(simulo)``.
271
+
272
+ That is ``__all__`` (eager platform exports + learning names) unioned with
273
+ :data:`LEARNING_NAMES`. Private module globals (``_pkgutil``,
274
+ ``_resolve_learning``, ``_LEARNING_NAMES``, the ``TYPE_CHECKING`` import, …)
275
+ are deliberately filtered out so tab-completion shows the user-facing API only.
276
+ """
277
+ return sorted(set(__all__) | set(_LEARNING_NAMES))
278
+
279
+
280
+ if TYPE_CHECKING:
281
+ # Static aliases so a type checker sees the learning names with precise types
282
+ # (PEP 562 ``__getattr__`` would otherwise type them as ``Any``). The rule that
283
+ # this block MUST follow — and that the mypy positive test in
284
+ # ``tests/test_learning_typing.py`` locks — is:
285
+ #
286
+ # * **Subclassable** names (used as ``class X(simulo.Task)``) alias their
287
+ # ``interfaces.runtime`` Protocol, so the subclass is checked against the
288
+ # contract (e.g. ``TaskProtocol``).
289
+ # * **Instantiated mirror** names (used as ``simulo.LearningEnv(task=...)``,
290
+ # ``simulo.RLTrainer(env=...)``, ``simulo.Robot(...)``,
291
+ # ``simulo.RLPlayer(env=...)``) alias their typing FACADE in the thin
292
+ # client's internal facades module — a Protocol alias would make mypy
293
+ # reject instantiation ("Cannot instantiate protocol class"), and
294
+ # ``Any`` (the pre-Wave-C4 rule) type-checked nothing. The facade
295
+ # declares the contract surface plus the real constructor, so both the
296
+ # call and every member access are checked against the real member set
297
+ # and signature shape (name, kind, arity, presence of a default —
298
+ # NOT the declared type; see the facades module docstring for the
299
+ # known gap); facade fidelity to the contract AND to the concrete
300
+ # worker class is pinned by the backend suite's conformance engine.
301
+ # * **Called mirror** name (``simulo.run(scenario_class, ...)`` — the one
302
+ # non-learning, non-instantiated member of this kind): same problem as
303
+ # an instantiated mirror one level down — with no override, ``run``
304
+ # types as ``object`` (``__getattr__``'s declared return), and
305
+ # ``object`` is not *callable* at all. It aliases a plain FUNCTION
306
+ # facade (there is no Protocol for a bare function to alias against),
307
+ # never registered in the backend's class-shaped conformance engine —
308
+ # see the facades module docstring for where its drift guard lives
309
+ # instead.
310
+ # * **Annotation-position mirror** names alias their contract Protocol:
311
+ # ``Scene`` is ``SceneProtocol`` — measured across every shipped user
312
+ # surface it appears only as ``def build(self, scene: simulo.Scene)``,
313
+ # never instantiated, and the Protocol alias is what keeps that override
314
+ # Liskov-compatible with the contract's ``scene: SceneProtocol``.
315
+ # * **Relocated value types** alias their real, dependency-free
316
+ # ``simulo.interfaces.authoring`` class — precise field checking in both
317
+ # modes, because only one definition exists.
318
+ #
319
+ # Engine authoring vocabulary (the one-import rule's second half —
320
+ # ``Camera`` and its sensor siblings, ``Simulation``, the controllers, the
321
+ # actuators, the ``*Set`` containers): only ``Camera`` gets an alias here. ``CameraProtocol`` exists
322
+ # in ``simulo.interfaces.authoring`` and ``Camera`` is instantiated
323
+ # (``simulo.Camera(width=640, ...)``, never subclassed), so it follows the
324
+ # "instantiated mirror -> facade" rule above exactly. The other 17 names
325
+ # have no TYPE_CHECKING alias, DELIBERATELY, per the rule's own "where none
326
+ # exists, do not invent one" corollary:
327
+ # - ``CameraSpawnConfig`` / ``SensorOffset`` / ``RayPattern`` /
328
+ # ``ActuatorGainsConfig`` / ``GripperState`` / ``Simulation`` have no
329
+ # contract Protocol at all — nothing to alias against.
330
+ # - ``ContactSensor`` / ``RayCaster`` / ``IMU`` / ``ForceTorqueSensor`` /
331
+ # ``FrameTransformer`` conform only to the BASE ``SensorProtocol``
332
+ # (path/update_period/enabled/debug_vis/name/update/reset) — it does not
333
+ # declare their type-specific readers (``read_net_forces``,
334
+ # ``read_hits``, ``read_linear_acceleration``, …), so aliasing them to
335
+ # ``SensorProtocol`` would be a LOSSY, misleading type (mypy would flag
336
+ # real shipped calls to those readers as errors) and aliasing to a
337
+ # Protocol at all breaks instantiation ("Cannot instantiate protocol
338
+ # class") — the same reason facades exist. Their own per-type contracts
339
+ # are a later decision (``simulo.interfaces.authoring.sensor``'s own
340
+ # docstring), not implied here.
341
+ # - ``ObservationSet`` / ``RewardSet`` / ``TerminationSet`` /
342
+ # ``DifferentialIKController`` / ``OperationalSpaceController`` /
343
+ # ``SurfaceGripperActuator`` have no dedicated Protocol either.
344
+ # Runtime resolution (the lazy resolver) is unaffected either way — these
345
+ # fall back to ``__getattr__``'s ``-> object`` return type for a type
346
+ # checker, same as any name with no static override.
347
+ from simulo._client.facades import Camera as Camera
348
+ from simulo._client.facades import LearningEnv as LearningEnv
349
+ from simulo._client.facades import RLPlayer as RLPlayer
350
+ from simulo._client.facades import RLTrainer as RLTrainer
351
+ from simulo._client.facades import Robot as Robot
352
+ from simulo.interfaces.authoring import SceneProtocol
353
+ from simulo.interfaces.runtime import (
354
+ ObservationComponentProtocol,
355
+ PlayerProtocol,
356
+ PolicyProtocol,
357
+ RewardComponentProtocol,
358
+ ScenarioProtocol,
359
+ TaskProtocol,
360
+ TerminationConditionProtocol,
361
+ TrainerProtocol,
362
+ )
363
+
364
+ # Subclassable names alias their Protocol so ``class X(simulo.Task)`` is checked
365
+ # against ``TaskProtocol``.
366
+ Task = TaskProtocol
367
+ Scenario = ScenarioProtocol
368
+ Policy = PolicyProtocol
369
+ Player = PlayerProtocol
370
+ Trainer = TrainerProtocol
371
+ ObservationComponent = ObservationComponentProtocol
372
+ RewardComponent = RewardComponentProtocol
373
+ TerminationCondition = TerminationConditionProtocol
374
+ # Annotation-position mirror name (see the rule above).
375
+ Scene = SceneProtocol
376
+ # ``save_artifact`` aliases its real execution-mode implementation (a
377
+ # torch-free thin-client function), so ``simulo.save_artifact("summary",
378
+ # path)`` type-checks with the precise signature instead of ``Any``.
379
+ from simulo._client.artifacts import save_artifact as save_artifact
380
+
381
+ # ``run`` (the scenario execution loop) aliases its typing FACADE — a
382
+ # plain FUNCTION facade, not a class one (there is no ``RunProtocol`` to
383
+ # alias against; ``run`` is not a contract method). Without this, ``run``
384
+ # falls back to ``__getattr__``'s ``-> object`` return type, and
385
+ # ``object`` is not callable: ``simulo.run(MyScenario, ...)`` in a freshly
386
+ # scaffolded app is a hard mypy error no ``--ignore-missing-imports`` can
387
+ # suppress. The facade's real target (``simulo.scenario.run``) lives in
388
+ # the ``simulo-backend`` distribution — outside this package's own
389
+ # ``MYPYPATH`` — but the facade itself is thin-client-resident code
390
+ # (``simulo/_client/facades.py``), exactly like ``Camera``'s facade for
391
+ # the same reason; see that module's docstring for the full rationale and
392
+ # where drift against the real function is caught (a live, unmocked
393
+ # signature comparison in ``simulo-backend``'s test suite, not mypy).
394
+ from simulo._client.facades import run as run
395
+
396
+ # RELOCATED authoring value types (Plan C — Waves C1, C2, then C4's
397
+ # ``RecordConfig``): the runtime object IS the real, dependency-free
398
+ # ``simulo.interfaces.authoring`` class in both
399
+ # modes, so these alias the real class — precise field/signature checking
400
+ # (e.g. mypy rejects ``simulo.Pose(xyz="nope")``) instead of ``Any``.
401
+ from simulo.interfaces.authoring import Entity as Entity
402
+ from simulo.interfaces.authoring import Light as Light
403
+ from simulo.interfaces.authoring import Material as Material
404
+ from simulo.interfaces.authoring import Physics as Physics
405
+ from simulo.interfaces.authoring import Pose as Pose
406
+ from simulo.interfaces.authoring import RecordConfig as RecordConfig
407
+ from simulo.interfaces.authoring import Terrain as Terrain
408
+ from simulo.interfaces.authoring import Visual as Visual
409
+ from simulo.interfaces.authoring import World as World
@@ -23,8 +23,9 @@ The on-disk :attr:`path` only exists in **execution mode**:
23
23
  name a robot/object source to load into a scene. In discovery (submit) these return
24
24
  an inert, torch-free :class:`_AssetSpec` so authoring a task is lean; only the
25
25
  backend runner (execution mode) resolves them to the real model-source
26
- ``simulo.interfaces.authoring.asset.Asset`` (the single, dependency-free
27
- definition the backend consumes — relocated out of the backend in Plan C).
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).
28
29
 
29
30
  **Layer split.** This is the user-facing *handle* (``simulo.Asset``); the
30
31
  asset-catalog **wire** contract (ref grammar, routes, caps, wire dataclasses) lives
@@ -40,7 +41,7 @@ from typing import Any, Iterator, List, Optional, Tuple
40
41
 
41
42
  from simulo._client._mounts import asset_env_key
42
43
  from simulo._client.mode import EXECUTION, current_mode
43
- from simulo.interfaces.authoring import Asset as _ModelSourceAsset
44
+ from simulo.interfaces.authoring import AssetSource as _ModelSourceAsset
44
45
  from simulo.interfaces.exceptions import ContractViolationError, ResourceNotFoundError
45
46
  from simulo.interfaces.platform.asset_catalog import (
46
47
  GLOBAL_ASSET_PUBLISHER,
@@ -181,8 +182,8 @@ class Asset:
181
182
  @staticmethod
182
183
  def _model_asset(kind: str, path: str) -> Any:
183
184
  if current_mode() == EXECUTION:
184
- # The model-source Asset is the dependency-free
185
- # ``simulo.interfaces.authoring.asset.Asset`` (single definition,
185
+ # The model-source AssetSource is the dependency-free
186
+ # ``simulo.interfaces.authoring.asset.AssetSource`` (single definition,
186
187
  # relocated out of the backend in Plan C) — importable statically
187
188
  # and torch-free, so no lazy import is needed any more.
188
189
  factory = getattr(_ModelSourceAsset, kind)
@@ -574,9 +574,9 @@ def upload_archive(
574
574
  uploaded = 0
575
575
  for start in range(0, len(needed), _MAX_PART_NUMBERS_PER_BATCH):
576
576
  batch = needed[start : start + _MAX_PART_NUMBERS_PER_BATCH]
577
- specs = {int(spec["part_number"]): spec for spec in client.presign_parts(version_id, batch)}
577
+ specs_by_number = {int(spec["part_number"]): spec for spec in client.presign_parts(version_id, batch)}
578
578
  for part_number in batch:
579
- spec = specs.get(part_number)
579
+ spec = specs_by_number.get(part_number)
580
580
  if spec is None:
581
581
  raise AssetApiError(f"The server did not presign part {part_number}.")
582
582
  chunk = _read_chunk(tar_path, part_number, part_size)
@@ -66,6 +66,7 @@ import xml.etree.ElementTree as ET
66
66
  from dataclasses import dataclass, field
67
67
  from hashlib import sha256
68
68
  from pathlib import Path, PurePosixPath
69
+ from types import ModuleType
69
70
  from typing import Callable, Iterable, Optional
70
71
 
71
72
  import defusedxml.ElementTree as DefusedET
@@ -132,7 +133,7 @@ class AssetsExtraMissingError(AssetPackageError):
132
133
  ASSETS_EXTRA_INSTALL_HINT = 'pip install "simulo[assets]"'
133
134
 
134
135
 
135
- def _load_pxr_usdutils(): # pragma: no cover - exercised via _extract_usd_references seam
136
+ def _load_pxr_usdutils() -> ModuleType: # pragma: no cover - exercised via _extract_usd_references seam
136
137
  """Import ``pxr.UsdUtils`` lazily — the ONLY place ``pxr`` is ever imported.
137
138
 
138
139
  Raises :class:`AssetsExtraMissingError` with the copyable install command
@@ -145,7 +146,11 @@ def _load_pxr_usdutils(): # pragma: no cover - exercised via _extract_usd_refer
145
146
  "Publishing or validating a USD package needs the USD inspection extra. "
146
147
  f"Install it with: {ASSETS_EXTRA_INSTALL_HINT}"
147
148
  ) from exc
148
- return UsdUtils
149
+ # `pxr` publishes no stubs (see the mypy override in pyproject.toml), so
150
+ # `UsdUtils` arrives as Any. Bind through a typed local so this function
151
+ # hands its caller a real ModuleType instead of leaking Any across the seam.
152
+ usd_utils: ModuleType = UsdUtils
153
+ return usd_utils
149
154
 
150
155
 
151
156
  def _extract_usd_references(layer_path: Path) -> list[str]:
@@ -905,10 +910,21 @@ def _inspect_urdf(
905
910
  "declarations and re-run."
906
911
  ) from exc
907
912
 
913
+ # ElementTree types getroot() as Optional because a tree can be constructed
914
+ # empty; a tree returned by a SUCCESSFUL parse always has a root, so this
915
+ # branch is unreachable in practice. It is a real check rather than a cast
916
+ # so that if it ever were reached, it surfaces as the same clean
917
+ # publish-time error as every other URDF problem — never an AttributeError.
918
+ root_element = tree.getroot()
919
+ if root_element is None: # pragma: no cover - unreachable after a successful parse
920
+ raise AssetPackageError(
921
+ f"{entry_rel} is not parseable as URDF XML: it has no root element. Fix the file and re-run."
922
+ )
923
+
908
924
  closure: dict[str, Path] = {entry_rel: entry_abs}
909
925
  reference_count = 0
910
926
  rewrites: list[tuple[ET.Element, str]] = [] # (element, new entry-relative filename)
911
- for element in tree.getroot().iter():
927
+ for element in root_element.iter():
912
928
  if element.tag not in ("mesh", "texture"):
913
929
  continue
914
930
  ref = element.get("filename")
@@ -935,7 +951,7 @@ def _inspect_urdf(
935
951
  if reference_count > 0:
936
952
  for element, new_filename in rewrites:
937
953
  element.set("filename", new_filename)
938
- entry_override_bytes = ET.tostring(tree.getroot(), encoding="utf-8", xml_declaration=True)
954
+ entry_override_bytes = ET.tostring(root_element, encoding="utf-8", xml_declaration=True)
939
955
 
940
956
  files = sorted(closure.items(), key=lambda item: item[0])
941
957
  return entry_rel, auto, files, reference_count, warnings, entry_override_bytes
@@ -20,7 +20,7 @@ The heavy logic (resolution, gating, rendering) lives here rather than in
20
20
  from __future__ import annotations
21
21
 
22
22
  import sys
23
- from typing import Dict, List, Optional, Sequence, TextIO
23
+ from typing import Any, Dict, List, Mapping, Optional, Sequence, TextIO
24
24
 
25
25
  from simulo._client.runtime_display import display_runtime
26
26
  from simulo._client.submit_api import AssetPinError, SubmitApiClient
@@ -34,7 +34,7 @@ from simulo.interfaces.platform.asset_catalog import DEFAULT_ASSET_RUNTIME, Reso
34
34
  __all__ = ["DEFAULT_ASSET_RUNTIME", "collect_asset_refs", "resolve_and_pin_assets"]
35
35
 
36
36
 
37
- def collect_asset_refs(manifest: Dict) -> List[str]:
37
+ def collect_asset_refs(manifest: Mapping[str, Any]) -> List[str]:
38
38
  """Return the de-duplicated registry-asset refs an app declared, in first-seen
39
39
  order.
40
40
 
@@ -85,7 +85,7 @@ def _resolved_publisher(canonical_ref: str) -> Optional[str]:
85
85
  return None
86
86
 
87
87
 
88
- def _render_resolved_block(refs: Sequence[str], resolved: Dict[str, ResolvedAssetPin]) -> str:
88
+ def _render_resolved_block(refs: Sequence[str], resolved: Mapping[str, ResolvedAssetPin]) -> str:
89
89
  """The ``Resolved assets:`` block — the authored (short) ref →
90
90
  resolved version + short digest, columns aligned, in the order the refs were
91
91
  declared. When the ref OMITTED its publisher (the caller's own org), the