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 typedobjectand 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.
subsetsandredefines. Reported by the service and available onSymbol.specializations, but onlyspecializesbecomes 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
enumDefbecomes a plain class rather than a PythonEnum. - Name collisions. Two definitions with the same simple name both get
path-qualified class names (
A_Thing,B_Thing). A feature named like a memberTypedObjectprovides (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-verifiessysml-grpcconnection.py— gRPC channel, service lifecycle, cross-process refcountingmodel.py— a parsed model: root symbol and diagnosticssymbol.py— lazy symbol proxy, fetches children on demandinstance.py— instantiated object and its slotstypefacts.py— a symbol's static type, multiplicity and supertypestyped.py— base class and slot decoders the generated classes are built ongenerate.py— emits typed classes from a parsed modeldiagnostic.py— one diagnostic with its source locationproto/— 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
af363db6646f4c94d3d31394c74737a980e485f57c980b3fe8e3bc595de63d25
|
|
| MD5 |
55e988acb60950d58f3a783e9c351cfc
|
|
| BLAKE2b-256 |
9f840d95e29f2c5a5db5463bda96dd843446dbd9918c14ef05c8064cc5a6e02a
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
42c8489f1921fecf4d40cc6cd50482c57d5bfc2b17a503c95d120774f982b43a
|
|
| MD5 |
1381d2227e4ebf6ca0a2342ec5276121
|
|
| BLAKE2b-256 |
495856d09a598356afcfc8f5b9adc869369f9a62dbd3ffbc53517d7a71a0d408
|