Declarative modelling¶
Added in version 0.5.0: Introduced the declarative modelling module.
py-capellambse supports declarative modelling with the capellambse.decl
module. This requires the optional dependency capellambse[decl]
to be
installed.
The YAML-based declarative modelling engine combines a few simple concepts into a powerful, but easy to use file format. These files can then be applied to any model supported by py-capellambse.
Example¶
Here is an example YAML file that declares a simple coffee machine with a couple of functions, and functional exchanges between them:
1- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440 # root logical component
2 extend:
3 components:
4 - name: Coffee Machine
5 allocated_functions:
6 - !promise brew coffee
7- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d # root logical function
8 extend:
9 functions:
10 - name: brew coffee
11 promise_id: brew coffee
12 inputs:
13 - name: Portafilter port
14 - name: Steam port
15 promise_id: steam input
16 outputs:
17 - name: Fluid port
18 - name: Waste port
19 promise_id: waste output
20 - name: produce steam
21 inputs:
22 - name: Water port
23 - name: Power port
24 - name: User command
25 outputs:
26 - name: Steam port
27 promise_id: steam output
28 - name: collect process waste
29 inputs:
30 - name: Waste port
31 promise_id: waste input
32 exchanges:
33 - name: Steam
34 source: !promise steam output
35 target: !promise steam input
36 - name: Waste
37 source: !promise waste output
38 target: !promise waste input
CLI usage¶
If the additional optional dependency capellambse[decl,cli]
is installed,
this file can be applied from the command line. Assuming it is saved as
coffee-machine.yml
, it can then be applied to a locally stored Capella
model like this:
python -m capellambse.decl --model path/to/model.aird coffee-machine.yml
Refer to the capellambse.cli_helpers.loadcli()
documentation to find
out the supported argument format for --model
.
API usage¶
Declarative YAML can also be applied programmatically, by calling the
capellambse.decl.apply()
function. It takes a (loaded) py-capellambse
model, and either a path to a file or a file-like object. To pass in a string
containing YAML, wrap it in io.StringIO
:
import io, capellambse.decl
my_model = capellambse.MelodyModel(...)
my_yaml = "..."
capellambse.decl.apply(my_model, io.StringIO(my_yaml))
my_model.save()
Format description¶
The YAML file may contain one or two YAML documents (separated by a line
containing only three minus signs ---
). The first document contains
metadata, while the second document contains the instructions to perform
against the model. The metadata document may be omitted, in which case the file
only contains an instruction stream.
Metadata¶
Added in version 0.6.8: Added metadata section to the declarative modelling YAML.
The metadata section is optional and has the following format:
model:
url: https://example.com/model.git
revision: 0123456789abcdefdeadbeef0123456789abcdef
entrypoint: path/to/model.aird
written_by:
capellambse_version: 1.0.0
generator: Example Generator 1.0.0
It contains information about which model the declarative modelling YAML file
wants to change, and which capellambse version and generator it was written
with. A versioned model can be uniquely identified by its repository URL, the
revision, and the model entrypoint. decl.apply()
with strict=True
will
verify these values against the model.info
of the passed model.
Instructions¶
The expected instruction document in the YAML follows a simple format, where a parent object (i.e. an object that already exists in the model) is selected, and one or more of three different operations is applied to it:
extend
-ing the object on list attributes,set
-ting properties on the object itself,sync
-ing objects into the model, ordelete
-ing one or more children.
Selecting a parent¶
There are a few ways to select a parent object from the model.
The most straight-forward way is to use the universally unique ID (UUID), using
the !uuid
YAML tag. The following query selects the root logical function
in our test model:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
A more versatile way involves the !find
YAML tag, which allows specifying a
set of attributes in order to filter down to a single model element. This tag
simply takes a mapping of all the attributes to select for. This usually also
involves the element’s type (or class), which is selectable with the _type
attribute:
- parent: !find
_type: LogicalFunction
# ^-- note the leading underscore, to disambiguate from the "type"
# property that exists on some model elements
name: Root Logical Function
set: [...]
The !find
tag also supports dot-notation for filtering on nested
attributes.
- parent: !find
_type: FunctionOutputPort
name: FOP 1
owner.name: manage the school
set: [...]
Extending objects¶
The following subsections show how to create completely new objects, or
reference and move already existing ones, using examples of declarative YAML
files on ElementListCouplingMixin
-ish
attributes. The extension of one-to-one attributes works in the same way,
adhering to the YAML syntax.
Creating new objects¶
LogicalFunction
objects have several
different attributes which can be modified from a declarative YAML file. For
example, it is possible to create new
sub-functions
. This
snippet creates a function with the name “brew coffee” directly below the root
function:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
extend:
functions:
- name: brew coffee
Functions can be nested arbitrarily deeply, and can also receive any other supported attributes at the same time. The “brew coffee” function for example could further receive nested child functions, each providing an output port:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
extend:
functions:
- name: brew coffee
functions:
- name: grind beans
outputs:
- name: Ground Beans port
- name: heat water
outputs:
- name: Hot Water port
While objects that already exist in the base model can be referenced with
!uuid
, this is not possible for objects declared by the YAML file, as they
will have a random UUID assigned to ensure uniqueness. For this reason, a
promise mechanic exists, which allows to “tag” any declared object with a
promise_id
, and later reference that object with the !promise
YAML tag.
These promise IDs are user defined strings. The only requirement is that two
objects cannot receive the same ID, however they can be referenced any number
of times. This example snippet demonstrates how to declare two logical
functions, which communicate through a functional exchange:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
extend:
functions:
- name: brew coffee
inputs:
- name: Steam port
promise_id: steam-input
- name: produce steam
outputs:
- name: Steam port
promise_id: steam-output
exchanges:
- name: Steam
source: !promise steam-output
target: !promise steam-input
The !promise
tag (and the !uuid
tag as well) can be used anywhere where
a model object is expected.
Creating new references¶
It is important to understand when new model objects are created and when only
references are added. The following example would create a reference in the
.allocated_functions
attribute of the
LogicalComponent
which is also the
logical root_component
(parent) to the logical root_function
:
- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440
extend:
allocated_functions:
- !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
This is caused by the type of relationship (non-
DirectProxyAccessor
) between the parent and its
allocated_functions
.
It is also possible to create references to promised objects, but extra caution
for declaring promise_id
s for resolving these promises successfully:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
extend:
functions:
- name: The promised one
promise_id: promised-fnc
- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440
extend:
functions:
- !promise promised-fnc
The promise_id
declaration can also happen after referencing it.
Moving objects¶
The following example would move a logical function from underneath a
LogicalFunctionPkg
(accessible via
functions
) into functions
of the logical root_function
(parent)
since the functions
attribute has a parent/children relationship (i.e. the
DirectProxyAccessor
is used).
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
extend:
functions:
- !uuid 8833d2dc-b862-4a50-b26c-6f7e0f17faef
Setting properties¶
After selecting a parent, it is also possible to directly change its properties
without introducing new objects into the model. This happens by specifying the
attributes in the set:
key.
The following example would change the name
of the root
LogicalComponent
to “Coffee Machine”
(notice how we use a different UUID than before):
- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440
set:
name: Coffee Machine
This is not limited to string attributes; it is just as well possible to change
e.g. numeric properties. This example changes the min_card
property of an
ExchangeItemElement
to 0
and
the max_card
to infinity, effectively removing both limitations:
- parent: !uuid 81b87fcc-03cf-434b-ad5b-ef18266c5a3e
set:
min_card: 0
max_card: .inf
Synchronizing objects¶
The sync:
key is a combination of the above extend:
and set:
keys.
Using it, it is possible to modify objects that already exist, or create new
objects if they don’t exist yet.
Unlike the other keys however, sync:
takes two sets of attributs: one that
is used to find a matching object (using the find:
key), and one that is
only used to set values once the object of interest was found (using the
set:
key).
This operation also supports Promise IDs, which are resolved either using the found existing object or the newly created one.
The following snippet ensures that the root LogicalFunction contains a subfunction “brew coffee” with the given description. A function named “brew coffee” will be used if it already exists, but if not, a new one will be created. In either case, the used function will resolve the “brew-coffee” promise, which can be used to reference it elsewhere:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d # the root function
sync:
functions:
- find:
name: brew coffee
set:
description: This function brews coffee.
promise_id: brew-coffee
- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440 # the root component
extend:
allocated_functions:
- !promise brew-coffee
Deleting objects¶
Finally, with declarative modelling files, it is possible to delete objects from the model. Depending on where the delete operation occurs, either the target object is deleted entirely, or only the link to it is destroyed.
Currently, objects to be deleted can only be selected by their UUID.
For example, this snippet deletes the logical function named “produce Great Wizards” from the model:
- parent: !uuid f28ec0f8-f3b3-43a0-8af7-79f194b29a2d
delete:
functions:
- !uuid 0e71a0d3-0a18-4671-bba0-71b5f88f95dd
In contrast, this snippet only removes its allocation to the “Hogwarts” root component, but the function still exists afterwards:
- parent: !uuid 0d2edb8f-fa34-4e73-89ec-fb9a63001440
delete:
allocated_functions:
- !uuid 0e71a0d3-0a18-4671-bba0-71b5f88f95dd