Query API

New in v0.11.0

The Query API is the part of Syside Automator for asking questions about a model from Python. Find the element with a given name, list what a part contains, collect every usage of a definition, check the constraints a model asserts, and turn the rules a model must follow into tests. It consists of three packages: query for the questions, evaluation for checking asserted constraints, and testing for writing rules as pytest tests. Every function takes a loaded model or an element from it and returns ordinary Python values: lists, booleans, numbers, strings or None.

You need Syside Automator installed and a license activated; see Install Automator. If you have never loaded a model from Python, Models as Python objects shows what a loaded model looks like.

Try it

Save the script below as rover.py and run python rover.py. It carries its own model as a string, so nothing else is needed:

import syside
from syside import query

MODEL = """
package Mission {
    part def Wheel;
    part def Motor;
    part def Rover {
        part wheels : Wheel [4];
        part motor : Motor;
        part spare : Wheel;
    }
}
"""


def main() -> None:
    (model, diagnostics) = syside.load_model(sysml_source=MODEL)
    assert not diagnostics.contains_errors(warnings_as_errors=True)

    # Find a definition by name, then everything typed by it.
    wheel = query.find_by_name_and_type(model, "Wheel", syside.PartDefinition)
    assert wheel is not None
    for part in query.find_elements_of_type(model, syside.PartUsage):
        if query.specializes(part, wheel):
            print(f"{part.qualified_name} is a Wheel")

    # List what a definition contains, and how many of each.
    rover = query.find_by_name_and_type(model, "Rover", syside.PartDefinition)
    assert rover is not None
    for child in query.contents(rover):
        if not isinstance(child, syside.PartUsage):
            continue
        bounds = query.multiplicity_bounds(child)
        count = "1" if bounds is None else f"{bounds.lower}..{bounds.upper}"
        print(f"{rover.name} has {count} {child.name}")


if __name__ == "__main__":
    main()

It prints:

Mission::Rover::wheels is a Wheel
Mission::Rover::spare is a Wheel
Rover has 4..4 wheels
Rover has 1 motor
Rover has 1 spare

Four functions did the work. find_by_name_and_type fetches one element by name and kind. find_elements_of_type lists every element of a kind in the whole model. specializes asks whether a part is typed by a definition, following the whole chain of specializations, so a part typed by a subtype of Wheel counts too. contents lists what an element directly owns, and multiplicity_bounds reads the [4] off a part. Replace the model string with paths=["your_model.sysml"] to run the same script against a file.

Where it helps most

  • Large models. A question written as a for loop over every element runs in time proportional to the model, and a script with twenty such loops over a model of millions of elements runs for hours. Build a NameIndex, TypeIndex or MetadataIndex once, pass it as index=, and each later lookup is a dictionary read. An index changes how long a query takes, never what it returns.

  • Checks that run on every change. check_model evaluates every assert constraint against the instances that bind values and returns a verdict per pair; the testing helpers report which elements break a rule, so the check can run in CI beside the code that depends on the model.

  • Reading a model safely. No function in syside.query changes the model. To create, edit or delete elements, use the element classes described in Models as Python objects; the two work on the same objects.

Where to go next

The complete list of functions, with the cost of each, is in the API reference.