Skip to main content

pysysml

Python client for Systemica: parse, inspect and execute SysML v2 models over the sysml-grpc service.

Installation

pip install pysysml             # from PyPI, once the first release is published
pip install -e python/          # or from a checkout, at the repository root

Dependencies (grpcio, protobuf>=7.35.1, filelock, psutil) come with it. They publish wheels for CPython 3.10 and later only, which is what requires-python says.

Getting the service binary

Every call goes through sysml-grpc. pysysml starts one for you and expects to find it at ~/.pysysml/bin/sysml-grpc. Three ways to put it there:

# 1. Download the release build (checksum-verified against its .sha256 sidecar)
python -c "from pysysml.binary import download_binary; download_binary('latest')"

# 2. Let pysysml.connect() download it on first use
export PYSYSML_GRPC_VERSION=latest      # or a tag like v0.0.5

# 3. Build from source
make build-grpc && mkdir -p ~/.pysysml/bin && cp bin/sysml-grpc ~/.pysysml/bin/

Without one of those, connect() raises ConnectionError rather than downloading anything unasked. PYSYSML_GITHUB_REPO overrides the repository releases are fetched from (default Open-MBEE/Systemica).

The published releases up to v0.0.4 carry the sysml/sysml-lsp archives only; sysml-grpc binaries are published from the next release onward, so until then build it from source (option 3).

Usage

import pysysml

model = pysysml.load("model.sysml")
for d in model.diagnostics:
    print(d)

print(pysysml.eval("1 + 2 * 3", file_path="model.sysml"))

Inspecting symbols

Model.find takes a short name and searches the symbol tree:

vehicle = model.find("Vehicle")   # not model.root.find(...), and not an FQN
vehicle.attributes()              # [Symbol(id='Demo::Vehicle::mass', kind='attributeUsage')]
vehicle.parts()                   # [Symbol(id='Demo::Vehicle::engine', kind='partUsage')]
vehicle.get_attr("mass")          # Symbol, or None if there is no such attribute

model.get("Demo::Vehicle") looks a symbol up by fully-qualified name instead.

A symbol also carries its static type facts, resolved by the service:

engine = model.get("Demo::Vehicle::engine")
engine.type_facts        # TypeFacts(declared='Engine', resolved_id='Demo::Engine', ...)
engine.multiplicity      # Multiplicity(lower='0', upper='1'), or None if undeclared
engine.specializations   # [Specialization(kind='typing', target_id='Demo::Engine', ...)]

Instances

Slot values come back as Python values, and a slot holding an object comes back as a nested Instance:

inst = pysysml.instantiate("Demo::Vehicle", model_hash=model.hash)

inst.mass                 # 1500.0
inst["mass"]              # 1500.0
inst.engine               # Instance(id=2, type='Demo::Engine', slots=1)
inst.engine.power         # 300.0
inst.slots                # {'mass': 1500.0, 'engine': Instance(...)}
inst.get("missing", 0)    # 0

Integers, reals, booleans, strings and sequences map to int, float, bool, str and list. Unknown names raise AttributeError (attribute access) or KeyError (item access), so hasattr, copy and pickle behave.

The service expands the object graph to depth 8 and stops at a type already on the path, so a part containing its own kind terminates; a child it did not expand comes back as its bare integer id rather than an Instance.

The raw protobuf stays reachable: get_slot(name) returns the SlotValue message, and raw_slots is the whole map.

inst.get_slot("mass").materialized         # True
inst.get_slot("engine").value.instance_id  # 2

A slot the service could not evaluate — a cyclic derived attribute, say — is never reported as None. Attribute and item access raise SlotError, while slots carries the SlotError as that entry's value so the rest of the instance stays inspectable.

cyclic.a             # raises SlotError: slot 'a': ... cyclic slot dependency
cyclic.slots["a"]    # SlotError(...)

SlotError is not an AttributeError, so hasattr on such a slot propagates it rather than returning False; use slots to inspect an instance whose slots may have failed.

eval returns a single value, so a result the wire format cannot represent raises UnsupportedValueError rather than being reported per entry.

execute_action and execute_state apply the same policy to their result maps: a value the wire format cannot represent is reported as an UnsupportedValueError in that entry, leaving the other entries intact.

Generated typed classes

Instance is dynamic, so an editor cannot complete inst.mass and a type checker cannot reject inst.mas. pysysml.generate emits a Python class per SysML definition, so both can:

python -m pysysml.generate internal/repl/testdata/vehicle_package.sysml -o demo_types.py
pysysml-generate model.sysml -o model_types.py     # same thing, as a console script
import pysysml
from demo_types import Vehicle

model = pysysml.load("internal/repl/testdata/vehicle_package.sysml")
inst = pysysml.instantiate("Demo::Vehicle", model_hash=model.hash)

v: Vehicle = Vehicle.from_instance(inst)   # a typed view over the Instance
v.mass                                      # 1500.0, typed float
v.engine.power                              # 300.0, through the generated Engine
v.instance                                  # the underlying Instance

mypy (or pyright) then reports v.mas as an unknown attribute and v.mass + "x" as an unsupported operand pair. pysysml ships a py.typed marker, so its own annotations are used too.

from_instance rejects an instance of another definition, naming both types, rather than failing later at attribute access. An instance of a definition that specializes the expected one is accepted, since its generated class derives from the expected class. An instance whose type no generated class describes is accepted: instantiating a usage reports the usage's own FQN (Demo::myCar, not Demo::SportsCar), which the client cannot relate to a definition, so rejecting it would break the ordinary way to obtain an instance. Vehicle.unchecked(inst) is the explicit escape hatch for a deliberately unchecked view.

Keeping a generated module honest

A generated module records what it came from, so a stale one can be detected rather than discovered at attribute access (or never, when a removed feature keeps type-checking):

SYSML_GENERATOR_VERSION = "1"   # emission schema of this generator
SYSML_MODEL_HASH = "sha256:…"   # hash of the model source it was generated from

--check regenerates in memory and compares, writing nothing:

python -m pysysml.generate model.sysml -o model_types.py --check   # exits 1 if stale

It exits non-zero when the module is missing or would change, naming the command that regenerates it, which makes it usable as a CI or pre-commit gate.

Generation requires a service that reports the type_facts capability, which it asks for over GetServerInfo. A service too old to answer that RPC, or one that answers without the capability, does not populate SymbolInfo.type_info, and generating against it would type every feature object — indistinguishable from a feature that is genuinely untyped. Generation therefore fails, naming the service in use, where it came from, and how to replace it, rather than emitting a silently useless module.

The generator emits a runtime .py, not a .py + .pyi pair: each feature is a property that carries the annotation and performs the delegation, so the types and the code that implements them cannot drift apart, and there is one artifact to commit. Output is deterministic — definitions ordered by fully-qualified name (base classes first), nothing environment-dependent written — so it can be committed and diffed; python/tests/golden/vehicle_types.py is exactly that.

Generated classes are views, not copies: attribute access goes to the underlying slot on every read, and Tier 1 behaviour is preserved. A slot that failed to evaluate raises SlotError; a slot holding a value of another type than the model declared raises TypeMismatchError rather than returning a wrongly typed value.

SysML → Python mapping

SysML Python
Real, Rational float
Integer, Natural int
Boolean bool
String str
usage typed by a definition that reduces to a library scalar (attribute def Celsius :> Real) that scalar (float)
usage typed by any other definition in the model that definition's generated class
multiplicity 1, 1..1, or undeclared X
multiplicity 0..1 X | None
*, 0..*, n..m with upper > 1 list[X]
Complex, Number object, with a comment naming the type
a type resolved outside the model (e.g. a library type) object, with a comment naming its FQN
an unresolved or absent type object, with a comment naming what was written
specializes a definition in the model Python base class

The fallback is always object and always says why in the property's docstring; no feature is given a type the model does not support, and Any is never used to dodge one.

Known limitations

  • Quantities. A value with a measurement unit (attribute mass = 1500.0 [kg]) is typed object and its docstring names the unit. The wire format has no magnitude-and-unit value, so the slot itself is reported as unsupported at runtime — this is a service limitation, not a codegen one.
  • Behavioral and connector usages. Only structural usages (attribute, part, item, occurrence, individual, port, enum) become properties. Action, state, calc, constraint, requirement, connection, flow, interface, allocation and case usages are not instance slots and are skipped.
  • subsets and redefines. Reported by the service and available on Symbol.specializations, but only specializes becomes a Python base class. A redefinition that narrows a feature's type is emitted with its own declared type, which Python does not check against the base property.
  • Multiple inheritance. Emitted in declaration order; a SysML hierarchy whose Python equivalent has no consistent MRO produces a module that fails to import.
  • Generics and enumerations. No generic parameters, and an enumDef becomes a plain class rather than a Python Enum.
  • Name collisions. Two definitions with the same simple name both get path-qualified class names (A_Thing, B_Thing). A feature named like a member TypedObject provides (instance, from_instance, sysml_id) gets a trailing underscore (instance_); the SysML slot name it reads is unchanged.

pysysml.connect(host, port, auto_start=True) returns a Connection when you want to manage the service yourself; the module-level functions share a lazily created singleton connection instead. The service is reference-counted across processes, so the last client to exit shuts it down.

Development

pytest python/tests/                    # unit tests
pytest -m integration python/tests/     # needs a running sysml-grpc

# Regenerate the committed golden generated file (needs a running sysml-grpc)
python -m pysysml.generate internal/repl/testdata/vehicle_package.sysml \
    -o python/tests/golden/vehicle_types.py

# Regenerate protobuf bindings (from the repository root)
pip install grpcio-tools
make python-proto

Modules

  • binary.py — locates, downloads and checksum-verifies sysml-grpc
  • connection.py — gRPC channel, service lifecycle, cross-process refcounting
  • model.py — a parsed model: root symbol and diagnostics
  • symbol.py — lazy symbol proxy, fetches children on demand
  • instance.py — instantiated object and its slots
  • typefacts.py — a symbol's static type, multiplicity and supertypes
  • typed.py — base class and slot decoders the generated classes are built on
  • generate.py — emits typed classes from a parsed model
  • diagnostic.py — one diagnostic with its source location
  • proto/ — generated message classes and stubs

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pysysml-0.1.0.tar.gz (75.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pysysml-0.1.0-py3-none-any.whl (46.9 kB view details)

Uploaded Python 3

File details

Details for the file pysysml-0.1.0.tar.gz.

File metadata

  • Download URL: pysysml-0.1.0.tar.gz
  • Upload date:
  • Size: 75.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for pysysml-0.1.0.tar.gz
Algorithm Hash digest
SHA256 af363db6646f4c94d3d31394c74737a980e485f57c980b3fe8e3bc595de63d25
MD5 55e988acb60950d58f3a783e9c351cfc
BLAKE2b-256 9f840d95e29f2c5a5db5463bda96dd843446dbd9918c14ef05c8064cc5a6e02a

See more details on using hashes here.

File details

Details for the file pysysml-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: pysysml-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 46.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.11.16

File hashes

Hashes for pysysml-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 42c8489f1921fecf4d40cc6cd50482c57d5bfc2b17a503c95d120774f982b43a
MD5 1381d2227e4ebf6ca0a2342ec5276121
BLAKE2b-256 495856d09a598356afcfc8f5b9adc869369f9a62dbd3ffbc53517d7a71a0d408

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page