simulo-interfaces 0.5.0__tar.gz → 0.6.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 (36) hide show
  1. {simulo_interfaces-0.5.0/src/simulo_interfaces.egg-info → simulo_interfaces-0.6.0}/PKG-INFO +1 -1
  2. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/pyproject.toml +1 -1
  3. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/__init__.py +6 -0
  4. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/asset_catalog.py +1 -1
  5. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/enums.py +6 -7
  6. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/runtime.py +12 -5
  7. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/submit.py +268 -1
  8. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0/src/simulo_interfaces.egg-info}/PKG-INFO +1 -1
  9. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/MANIFEST.in +0 -0
  10. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/PYPI.md +0 -0
  11. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/setup.cfg +0 -0
  12. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/__init__.py +0 -0
  13. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/exceptions.py +0 -0
  14. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/ids.py +0 -0
  15. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/app.py +0 -0
  16. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/asset.py +0 -0
  17. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/callbacks.py +0 -0
  18. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/debug.py +0 -0
  19. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/domain.py +0 -0
  20. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/runs.py +0 -0
  21. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/platform/volume.py +0 -0
  22. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/py.typed +0 -0
  23. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/__init__.py +0 -0
  24. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/anomaly.py +0 -0
  25. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/components.py +0 -0
  26. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/env.py +0 -0
  27. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/player.py +0 -0
  28. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/policy.py +0 -0
  29. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/scenario.py +0 -0
  30. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/task.py +0 -0
  31. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/tensors.py +0 -0
  32. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo/interfaces/runtime/trainer.py +0 -0
  33. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo_interfaces.egg-info/SOURCES.txt +0 -0
  34. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo_interfaces.egg-info/dependency_links.txt +0 -0
  35. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo_interfaces.egg-info/requires.txt +0 -0
  36. {simulo_interfaces-0.5.0 → simulo_interfaces-0.6.0}/src/simulo_interfaces.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo-interfaces
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Type contracts for the Simulo SDK. Installed automatically as a dependency of the simulo package.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simulo-interfaces"
7
- version = "0.5.0"
7
+ version = "0.6.0"
8
8
  description = "Type contracts for the Simulo SDK. Installed automatically as a dependency of the simulo package."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.11"
@@ -109,6 +109,7 @@ from simulo.interfaces.platform.submit import (
109
109
  MODEL_KINDS,
110
110
  PACKAGE_ARCHIVE_ROUTE_TEMPLATE,
111
111
  PACKAGES_ROUTE,
112
+ PLATFORM_PINNED_PACKAGES,
112
113
  RESERVED_RUNTIME_ENV_KEYS,
113
114
  RESERVED_RUNTIME_ENV_PREFIXES,
114
115
  SEED_JOB_REF_GRAMMAR,
@@ -125,6 +126,8 @@ from simulo.interfaces.platform.submit import (
125
126
  WORKER_LEASE_HEADER,
126
127
  ModelRecord,
127
128
  RecordingRecord,
129
+ canonical_pypi_name,
130
+ is_platform_pinned,
128
131
  )
129
132
  from simulo.interfaces.platform.volume import VolumeProtocol
130
133
 
@@ -181,6 +184,9 @@ __all__ = [
181
184
  "RecordingRecord",
182
185
  "RESERVED_RUNTIME_ENV_KEYS",
183
186
  "RESERVED_RUNTIME_ENV_PREFIXES",
187
+ "PLATFORM_PINNED_PACKAGES",
188
+ "is_platform_pinned",
189
+ "canonical_pypi_name",
184
190
  # explicit-run-intent plan: submit-time seed source (additive)
185
191
  "SEED_SOURCE_FIELDS",
186
192
  "SEED_SOURCE_CONFLICT_CODE",
@@ -256,7 +256,7 @@ GLOBAL_ASSET_PUBLISHER = "simulo"
256
256
  #: optional group being absent, 3+ chars matches via the group, but exactly 2
257
257
  #: chars can match neither — incoherent for users. Fixed here to ``{0,61}``,
258
258
  #: which accepts every length 1–63 contiguously; see the PR-1 resolutions in
259
- #: ``context/implementation-plans/usd-asset-catalogs/api-contract-freeze.md``.)
259
+ #: ``context/implementation-plans/archive/usd-asset-catalogs/api-contract-freeze.md``.)
260
260
  #: Pinned here (rather than imported) because ``simulo-interfaces`` has zero
261
261
  #: dependencies on the control plane — the value is copied deliberately and this
262
262
  #: docstring is the drift anchor.
@@ -72,13 +72,12 @@ class ResourceKind(StrEnum):
72
72
  """A configuration bundle."""
73
73
 
74
74
  WORLD = "world"
75
- """A world asset from the USD Asset Catalogs feature an environment /
76
- facility / terrain / scene robots operate in. The catalog's ``world`` kind
77
- (see ``ASSET_KINDS`` in ``platform/asset_catalog.py``) and the legacy
78
- ``ENVIRONMENT`` resource-mount term denote the same concept; ``WORLD`` is the
79
- catalog vocabulary (§2 of the USD Asset Catalogs proposal — "world", not
80
- "environment", which is overloaded). Both members are wire-stable and
81
- retained (append-only); neither replaces the other."""
75
+ """A catalog world a facility, terrain, or scene robots operate in.
76
+
77
+ ``WORLD`` is the current catalog term. The older ``ENVIRONMENT`` resource
78
+ mount term denotes the same concept; both values remain wire-stable and
79
+ neither replaces the other.
80
+ """
82
81
 
83
82
 
84
83
  class ArtifactKind(StrEnum):
@@ -1,14 +1,14 @@
1
1
  """``RuntimeProtocol`` — Simulo-curated runtime contract (Part B, Rule #4).
2
2
 
3
3
  Runtimes are Simulo-owned, prebuilt execution environments. The contract is
4
- deliberately tiny: pick a curated runtime by name, and defer heavy imports
5
- behind ``imports()``. There is no dependency-declaration, user-environment,
6
- or image-customisation surface the platform runtime ships everything a job
7
- needs.
4
+ deliberately narrow: pick a curated runtime by name, merge in a guarded set
5
+ of user environment variables via ``env()``, and defer heavy imports behind
6
+ ``imports()``. There is no dependency-declaration or image-customisation
7
+ surface — the platform runtime ships everything a job needs.
8
8
  """
9
9
 
10
10
  from contextlib import AbstractContextManager
11
- from typing import Protocol, runtime_checkable
11
+ from typing import Mapping, Protocol, runtime_checkable
12
12
 
13
13
 
14
14
  @runtime_checkable
@@ -17,6 +17,12 @@ class RuntimeProtocol(Protocol):
17
17
 
18
18
  Simulo owns and builds every runtime; ``from_registry`` picks one of the
19
19
  curated runtimes by name (there is no bring-your-own-image path).
20
+ ``env()`` merges caller-supplied environment variables into the job's
21
+ execution environment (later calls win on key conflicts) — guarded: a
22
+ runner/worker-owned key (see
23
+ ``simulo.interfaces.platform.submit.RESERVED_RUNTIME_ENV_KEYS`` /
24
+ ``RESERVED_RUNTIME_ENV_PREFIXES``) is never honoured; enforcement lives at
25
+ the runner/worker, not in this implementation-free Protocol.
20
26
  ``imports()`` is the real remote-only-import boundary (Rule #3): locally it
21
27
  records deferred import intent; in the cloud it resolves the real imports.
22
28
  Heavy imports (torch and friends) live ONLY inside this context manager.
@@ -24,4 +30,5 @@ class RuntimeProtocol(Protocol):
24
30
 
25
31
  @classmethod
26
32
  def from_registry(cls, image: str) -> "RuntimeProtocol": ...
33
+ def env(self, variables: Mapping[str, str]) -> "RuntimeProtocol": ...
27
34
  def imports(self) -> AbstractContextManager[None]: ...
@@ -124,7 +124,7 @@ platform-standard shape: ``{"error": {"code": ..., "message": ..., "request_id":
124
124
  terminal status, artifact kind) is server-side business logic layered on
125
125
  top of this wire shape, not defined by this module — see the
126
126
  explicit-run-intent plan's "Server validation" section
127
- (``context/implementation-plans/explicit-run-intent-plan.md``).
127
+ (``context/implementation-plans/archive/explicit-run-intent-plan.md``).
128
128
 
129
129
  ## Client-facing: recordings
130
130
 
@@ -325,6 +325,7 @@ models; the list is empty, never an error.
325
325
  recordings upload.
326
326
  """
327
327
 
328
+ import re
328
329
  from dataclasses import dataclass
329
330
  from typing import Optional
330
331
 
@@ -723,3 +724,269 @@ RESERVED_RUNTIME_ENV_KEYS = frozenset(
723
724
  #: ``SIMULO_CHECKPOINT_KEEP_LAST`` / ``SIMULO_RESUME``) does not match any of
724
725
  #: these prefixes and remains legitimate user-supplied job env.
725
726
  RESERVED_RUNTIME_ENV_PREFIXES = ("SIMULO_WORKER_", "SIMULO_INTERNAL_", "SIMULO_VOLUME_", "SIMULO_ASSET_")
727
+
728
+
729
+ # --------------------------------------------------------------------------
730
+ # Platform-pinned packages
731
+ # --------------------------------------------------------------------------
732
+ #
733
+ # The governing principle: anything already in the base runtime image is
734
+ # denied — a user ``pip_install()`` (restored in a later PR of this wave) may
735
+ # ADD packages the base lacks, but may never OVERRIDE one the base already
736
+ # provides. If a job's layer resolves a different version of a package the
737
+ # base already ships, whichever version resolution order wins can silently
738
+ # break the runtime in ways that never surface in the job's own code. The
739
+ # canonical case (#285): a job's ``pip_install("numpy")`` pulled numpy 2.x,
740
+ # shadowing the platform's numpy-1.x install and breaking the runtime's
741
+ # compiled camera-sensor bindings — the job never imported numpy directly,
742
+ # so the failure was undiagnosable from the job's own traceback. This is the
743
+ # same class of hazard :data:`RESERVED_RUNTIME_ENV_KEYS` guards against for
744
+ # environment variables, applied to pip package names instead: a job must
745
+ # never be able to shadow a platform-pinned dependency.
746
+ #
747
+ # :data:`PLATFORM_PINNED_PACKAGES` is the CLIENT-SIDE FAST-FAIL SUBSET, not
748
+ # the full "anything already in base" set — it names the known-critical base
749
+ # packages we can enumerate statically (the torch family, numpy, the
750
+ # nvidia/CUDA wheels, the platform's own SDK distributions) so
751
+ # ``pip_install()`` (later PR) can reject an obviously-doomed layer at
752
+ # submit time, before a job ever reaches the cloud. It is deliberately
753
+ # curated-but-generous, not exhaustive: the AUTHORITATIVE enforcement lives
754
+ # in ``simulo-backend`` (later PR), which checks each user-requested package
755
+ # against the worker's actual base-env installed set at install time —
756
+ # that check is complete by construction (it reads the real base env, it
757
+ # doesn't hand-maintain a mirror of it) and is what closes the gap this
758
+ # static frozenset cannot.
759
+ #
760
+ # Engine/runtime-native libraries (e.g. an ``isaacsim`` / ``isaaclab`` /
761
+ # ``omni.*``-shaped dependency) are image-baked into the base runtime, not
762
+ # resolved via ``pip`` into site-packages at job time — there is no pip
763
+ # package name a user's ``pip_install()`` could collide with for those, so
764
+ # they are deliberately NOT listed here. The nvidia/CUDA wheels are the
765
+ # opposite case: torch's CUDA build pulls them in via a normal ``pip``
766
+ # dependency into site-packages (the ``nvidia-*-cu12`` family + ``triton``),
767
+ # so a job absolutely could shadow one — they belong in this set. Only
768
+ # packages a job could actually reach through a real ``pip install <name>``
769
+ # belong here, including the platform's own SDK distributions (a job pinning
770
+ # its own ``simulo``/``simulo-interfaces``/``simulo-backend`` version could
771
+ # silently desync the client and runtime contract versions).
772
+ PLATFORM_PINNED_PACKAGES = frozenset(
773
+ {
774
+ "torch",
775
+ "torchvision",
776
+ "torchaudio",
777
+ "numpy",
778
+ "simulo",
779
+ "simulo-interfaces",
780
+ "simulo-backend",
781
+ "triton",
782
+ "nvidia-cuda-runtime-cu12",
783
+ "nvidia-cuda-nvrtc-cu12",
784
+ "nvidia-cuda-cupti-cu12",
785
+ "nvidia-cudnn-cu12",
786
+ "nvidia-cublas-cu12",
787
+ "nvidia-cufft-cu12",
788
+ "nvidia-curand-cu12",
789
+ "nvidia-cusolver-cu12",
790
+ "nvidia-cusparse-cu12",
791
+ "nvidia-nccl-cu12",
792
+ "nvidia-nvtx-cu12",
793
+ "nvidia-nvjitlink-cu12",
794
+ }
795
+ )
796
+
797
+ #: Matches the CUDA-wheel naming shape torch's CUDA build actually pulls into
798
+ #: site-packages — ``nvidia-<lib>-cu<major>`` (``nvidia-cublas-cu12``,
799
+ #: ``nvidia-cusolver-cu13``, ...) — matched in addition to the exact names
800
+ #: enumerated in :data:`PLATFORM_PINNED_PACKAGES` above. CUDA major-version
801
+ #: bumps mint new package names (``-cu12`` becomes ``-cu13`` and so on), so a
802
+ #: shape match keeps every CUDA runtime wheel denied even as the exact-name
803
+ #: list drifts behind a new CUDA release. Deliberately NOT a broad ``nvidia-``
804
+ #: prefix: that wrongly caught legitimate nvidia *helper* packages that do not
805
+ #: shadow anything in the base runtime (``nvidia-ml-py``,
806
+ #: ``nvidia-dali-cuda120`` — note ``-cuda120`` is not the ``-cu<major>``
807
+ #: runtime-wheel suffix), which a job may genuinely want to add.
808
+ _NVIDIA_CUDA_WHEEL_PATTERN = re.compile(r"^nvidia-.+-cu\d+$")
809
+
810
+ # --------------------------------------------------------------------------
811
+ # Plain-PyPI requirement validation (shared: is_platform_pinned below AND the
812
+ # thin client's Runtime.pip_install() both call canonical_pypi_name — see its
813
+ # docstring for why this is a POSITIVE allowlist, not a denylist)
814
+ # --------------------------------------------------------------------------
815
+ #
816
+ # Review round 2 (security MEDIUM + critic MAJOR) replaced an earlier
817
+ # character/prefix DENYLIST here. A denylist only catches the forms its
818
+ # author enumerated; this one leaked on every form it didn't:
819
+ #
820
+ # * ``"numpy(>=1.0)"`` — a parenthesized version spec (legal PEP 508) split
821
+ # its bare name as ``"numpy("`` under the old boundary-character split,
822
+ # missing :data:`PLATFORM_PINNED_PACKAGES` entirely (security MEDIUM).
823
+ # * ``"evil-1.0-py3-none-any.whl"`` / ``"pkg.tar.gz"`` — a bare archive
824
+ # filename contains none of the old denylist's banned characters (no
825
+ # ``@``/``/``/``:``/``\\``, no ``git+``/``./``/``../`` prefix) and sailed
826
+ # through as a "clean" package name; the worker would then do a LOCAL
827
+ # wheel/sdist install — pip runs ``setup.py``/build hooks for those,
828
+ # arbitrary code execution (critic MAJOR).
829
+ # * ``"."``, ``"-e"``, ``"--no-index"``, ``"--pre"`` — a bare directory or a
830
+ # leading-dash pip OPTION likewise contains none of the banned characters;
831
+ # pip would either install the current directory (running ``setup.py``) or
832
+ # have an option string injected into its own argv (critic MAJOR).
833
+ #
834
+ # :func:`canonical_pypi_name` fixes the class of bug, not just these three
835
+ # instances: it is a POSITIVE grammar match for "plain, index-resolved PEP
836
+ # 508 requirement, no direct reference" — anything that does not match,
837
+ # including forms nobody has thought of yet, is rejected by construction.
838
+ # This is also the ONE place both consumers call: ``is_platform_pinned``
839
+ # below and ``simulo._client.runtime.Runtime.pip_install`` (thin client) —
840
+ # duplicating the rule in both packages is how one gets hardened here and
841
+ # missed there (critic MINOR, review round 2).
842
+
843
+ #: PEP 503-shaped bare identifier: one or more alphanumeric runs separated by
844
+ #: single ``.``/``_``/``-`` runs; first and last character always
845
+ #: alphanumeric (so a bare ``.``/``..`` never matches). Written as
846
+ #: alternating alnum-run/separator-run groups — not the more obvious
847
+ #: ``[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?`` — specifically to avoid that
848
+ #: form's ambiguous internal split (two adjacent classes sharing alnum
849
+ #: characters), which is a classic catastrophic-backtracking shape; this
850
+ #: form has none (each alternative consumes a disjoint character class).
851
+ _PEP503_NAME = r"[A-Za-z0-9]+(?:[._-]+[A-Za-z0-9]+)*"
852
+ _EXTRAS = rf"\[\s*{_PEP503_NAME}(?:\s*,\s*{_PEP503_NAME})*\s*\]"
853
+ _VERSION_CMP = r"(?:===|==|!=|<=|>=|~=|<|>)"
854
+ _VERSION_TOKEN = r"[A-Za-z0-9_.*+!-]+"
855
+ _VERSION_CLAUSE = rf"{_VERSION_CMP}\s*{_VERSION_TOKEN}"
856
+ _VERSION_CLAUSES = rf"{_VERSION_CLAUSE}(?:\s*,\s*{_VERSION_CLAUSE})*"
857
+ #: A version spec may optionally be wrapped in parens (``"numpy(>=1.0)"``) —
858
+ #: PEP 508 permits this form; it is the exact shape the old denylist's
859
+ #: name-boundary split missed (see the module note above).
860
+ _VERSION_SPEC = rf"(?:\(\s*{_VERSION_CLAUSES}\s*\)|{_VERSION_CLAUSES})"
861
+ #: Environment-marker body after ``;``. Not a full PEP 508 marker-grammar
862
+ #: parse (boolean ``and``/``or``, string-literal quoting rules) — a safe
863
+ #: CHARACTER allowlist is sufficient here: ``@`` ``/`` ``\\`` are not in this
864
+ #: class, so a marker can never smuggle a direct reference or a path — any
865
+ #: attempt fails the OVERALL match, same as everywhere else in this grammar.
866
+ _MARKER_BODY = r"[A-Za-z0-9_.,()'\" <>=!~-]+"
867
+ _MARKER = rf";\s*{_MARKER_BODY}"
868
+
869
+ #: The full grammar: ``name[extras](version-spec)?(; marker)?``, anchored
870
+ #: start-to-end (via :func:`canonical_pypi_name`'s ``.match`` against the
871
+ #: WHOLE stripped string) — nothing may follow that this grammar doesn't
872
+ #: account for.
873
+ _PLAIN_PYPI_SPEC = re.compile(
874
+ rf"^\s*(?P<name>{_PEP503_NAME})"
875
+ rf"(?:\s*(?P<extras>{_EXTRAS}))?"
876
+ rf"(?:\s*(?P<version>{_VERSION_SPEC}))?"
877
+ rf"(?:\s*{_MARKER})?"
878
+ r"\s*$"
879
+ )
880
+
881
+ #: Archive-file suffixes pip installs directly from a local/downloaded file
882
+ #: without touching the index. Checked explicitly (case-insensitive) because
883
+ #: a bare archive filename is otherwise a SYNTACTICALLY valid PEP 503 name —
884
+ #: ``"evil-1.0-py3-none-any.whl"`` matches :data:`_PLAIN_PYPI_SPEC` letter
885
+ #: for letter (hyphens and dots are legal name characters) — so the grammar
886
+ #: match alone cannot reject it; this suffix check is the other half of
887
+ #: closing that gap (a bare ``packaging.Requirement`` parse has the identical
888
+ #: gap: ``Requirement("evil.whl")`` also parses as a plain name).
889
+ #:
890
+ #: Review round 3 (critic MAJOR / security HIGH): the round-2 tuple named
891
+ #: only 6 suffixes — a subset of pip's real recognized-archive set
892
+ #: (``pip._internal.utils.filetypes.ARCHIVE_EXTENSIONS`` /
893
+ #: ``TAR_EXTENSIONS`` + ``ZIP_EXTENSIONS`` + ``.whl``). ``"evil-1.0.tar"``,
894
+ #: ``"pkg.tbz"``, ``"pkg.tar.xz"``, ``"pkg.txz"``, ``"pkg.tlz"``,
895
+ #: ``"pkg.tar.lz"``, and ``"pkg.tar.lzma"`` (``.tar`` especially — a
896
+ #: mainstream sdist format) all had none of the 6 listed suffixes, so they
897
+ #: read as a clean canonical name and passed BOTH :func:`is_platform_pinned`
898
+ #: and the thin client's ``pip_install`` reject — pip installs each of these
899
+ #: as a local sdist and runs its ``setup.py``. This tuple is now pip's
900
+ #: COMPLETE 12-extension archive set, hardcoded (never `import pip` here —
901
+ #: this package stays torch-free/dependency-light) plus ``.egg`` kept as a
902
+ #: harmless extra (not one of pip's 12, but also not a legitimate bare PyPI
903
+ #: name suffix).
904
+ _ARCHIVE_SUFFIXES = (
905
+ ".zip",
906
+ ".whl",
907
+ ".tar.bz2",
908
+ ".tbz",
909
+ ".tar.gz",
910
+ ".tgz",
911
+ ".tar",
912
+ ".tar.xz",
913
+ ".txz",
914
+ ".tlz",
915
+ ".tar.lz",
916
+ ".tar.lzma",
917
+ ".egg",
918
+ )
919
+
920
+
921
+ def canonical_pypi_name(spec: str) -> Optional[str]:
922
+ """Return the canonical (PEP 503-normalised) bare name if *spec* is a
923
+ well-formed, plain-PyPI-installable requirement; ``None`` otherwise.
924
+
925
+ A spec is valid ONLY if it matches ``name[extras](version-spec)?(;
926
+ marker)?`` in full, with no direct-reference syntax — this is a POSITIVE
927
+ allowlist (see the module note above for why a denylist is the wrong
928
+ shape for this check). ``None`` covers every non-plain-PyPI form: a
929
+ direct reference (``name @ url``; any ``@``/``/``/``\\`` anywhere in the
930
+ spec fails the grammar), a VCS ref (``git+...`` — the ``+`` after
931
+ ``git`` is not a legal name character, so this also fails the grammar), a
932
+ local path (bare ``.``/``..``, or anything containing a path separator),
933
+ a bare archive filename (:data:`_ARCHIVE_SUFFIXES`), or a leading-dash
934
+ pip option (``-e``, ``--no-index``, ``-r``).
935
+
936
+ Callers MUST treat ``None`` as "reject" — never as "safe" or "not
937
+ pinned": see :func:`is_platform_pinned` (fails closed, returns ``True``)
938
+ and ``simulo._client.runtime.Runtime.pip_install`` (raises
939
+ ``SystemExit``) — the ONE validator both packages call (no duplicated
940
+ rule to drift out of sync).
941
+ """
942
+ stripped = spec.strip()
943
+ if not stripped or stripped in (".", ".."):
944
+ return None
945
+ if stripped.startswith("-"):
946
+ return None
947
+ if stripped.lower().endswith(_ARCHIVE_SUFFIXES):
948
+ return None
949
+ match = _PLAIN_PYPI_SPEC.match(stripped)
950
+ if match is None:
951
+ return None
952
+ return re.sub(r"[-_.]+", "-", match.group("name").lower())
953
+
954
+
955
+ def is_platform_pinned(pkg_name: str) -> bool:
956
+ """Return ``True`` if ``pkg_name`` must be denied as platform-pinned.
957
+
958
+ Reduces *pkg_name* to its canonical bare name via
959
+ :func:`canonical_pypi_name` and denies (returns ``True``) if:
960
+
961
+ * *pkg_name* is not a well-formed, plain-PyPI requirement at all — a
962
+ direct reference, VCS ref, local path, archive filename, or pip
963
+ option. :func:`canonical_pypi_name` returns ``None`` for all of these;
964
+ this function fails CLOSED on ``None`` (denies) rather than treating
965
+ an unparseable spec as "not pinned" — an unverifiable spec could
966
+ install arbitrary content under a pinned package's name.
967
+ * the canonical name matches the CUDA-wheel shape (see
968
+ :data:`_NVIDIA_CUDA_WHEEL_PATTERN`): ``nvidia-<lib>-cu<N>`` is denied
969
+ for any CUDA major ``<N>``, while nvidia helper packages outside that
970
+ shape (``nvidia-ml-py``, ``nvidia-dali-cuda120``) are allowed.
971
+ * the canonical name is an exact (not prefix) member of
972
+ :data:`PLATFORM_PINNED_PACKAGES` — ``"torchmetrics"`` and
973
+ ``"simulo-console"`` do NOT match ``"torch"`` / ``"simulo"``.
974
+
975
+ Matching is case-insensitive and tolerant of the forms a real
976
+ ``pip_install()`` call can pass — extras, version specifiers (bare or
977
+ parenthesized), and environment markers are all stripped by
978
+ :func:`canonical_pypi_name` before comparison, so ``"Torch"``,
979
+ ``"torch(==2.1)"``, ``"simulo_interfaces"``, and
980
+ ``"Simulo-Interfaces==0.5.0"`` all match their canonical entry.
981
+
982
+ This function is the client-side fast-fail check only (see the module
983
+ note above) — a package that passes it may still collide with the base
984
+ image; the backend's install-time check against the real base-env
985
+ install set is authoritative.
986
+ """
987
+ normalized = canonical_pypi_name(pkg_name)
988
+ if normalized is None:
989
+ return True
990
+ if _NVIDIA_CUDA_WHEEL_PATTERN.match(normalized):
991
+ return True
992
+ return normalized in PLATFORM_PINNED_PACKAGES
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simulo-interfaces
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Type contracts for the Simulo SDK. Installed automatically as a dependency of the simulo package.
5
5
  Author-email: Simulo Team <team@simulo.ai>
6
6
  License: BSD-3-Clause