How to get the associations between classes with Py4C?

Hi,

I am a beginner with Py4C and the Capella Simplified Metamodel so I might have missed something obvious.

In a Py4C script, I’d like to access the associations that I have created between classes in a Data Package using the Association symbol in the Palette.

A basic example will be sufficient.

I have also created a Generalization association between two classes using the Generic Connection Creation Tool and I am looking to ways with Py4C to know that such association exists between them.

Thank you.

Hi,

Please find below an example. To test it, I simply created an empty Capella project with 2 classes (Class 1 and Class2) in the data package at the operational analysis level, and created an association and a generalization between Class1 and Class2.

Note that for the associations, I am using the API from the simplified metamodel. This may not be sufficient depending on what you’re looking for, as this part of the simplified meta-model is not finalized.
For the generalization, I am going through a Java Object. This is what one can do when the API is not finalized. To find out how to navigate in the Capella full meta-model using Java object, I use either the Interpreter view to write AQL queries, or I look at the “expert” tab in the property view for a given object.

include('workspace://Python4Capella/simplified_api/capella.py')
if False:
    from simplified_api.capella import *

aird_path = '/TestClass/TestClass.aird'
#aird_path = '/In-Flight Entertainment System/In-Flight Entertainment System.aird'

model = CapellaModel()
model.open(aird_path)

# gets the SystemsEngineering and print its name
se = model.get_system_engineering()
print("Model opened: "+se.get_name())

#Get All Classes from the data package in the Operational Analysis
myClasses = se.get_operational_analysis().get_data_pkg().get_owned_classes()
#Get the first Class
myFirstClass = myClasses[0]
#Get all properties (associations)
myAssociations = myFirstClass.get_contained_properties()
#Get the first Property
myAssociation = myAssociations[0]
#Get the types of the Property
myAssociationTypes = myAssociation.get_type()
#Get the first type
myAssociationType = myAssociationTypes[0]
#Print the name
print("The class is associated with: " + myAssociationType.get_name())

#Get all Generalization links
myGeneralizations = myFirstClass.get_java_object().getOwnedGeneralizations()
#Get the first one
myGeneralization = myGeneralizations[0]
#Get the source
mySubClass = Class(myGeneralization.getSub())
#Get the target
mySuperClass = Class(myGeneralization.getSuper())
print("Generalization subClass: " + mySubClass.get_name())
print("Generalization SuperClass: " +  mySuperClass.get_name())

Stephane Lacrampe
Obeo Canada

Thank you for this prompt and sound response. I will try it and confirm here that it solves my problem.

I ran the code and I got this error which traces to “myFirstClass = myClasses[0]”

org.eclipse.emf.common.util.BasicEList$BasicIndexOutOfBoundsException: index=0, size=0
at org.eclipse.emf.common.util.BasicEList.get(BasicEList.java:346)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at py4j.reflection.MethodInvoker.invoke(MethodInvoker.java:244)
at py4j.reflection.ReflectionEngine.invoke(ReflectionEngine.java:357)
at py4j.Gateway.invoke(Gateway.java:282)
at py4j.commands.AbstractCommand.invokeMethod(AbstractCommand.java:132)
at py4j.commands.CallCommand.execute(CallCommand.java:79)
at py4j.ClientServerConnection.sendCommand(ClientServerConnection.java:244)
at py4j.CallbackClient.sendCommand(CallbackClient.java:384)
at py4j.CallbackClient.sendCommand(CallbackClient.java:356)
at py4j.reflection.PythonProxyHandler.invoke(PythonProxyHandler.java:106)
at jdk.proxy11/jdk.proxy11.$Proxy24.executeScript(Unknown Source)
at org.eclipse.ease.lang.python.py4j.internal.Py4jScriptEngine.internalExecute(Py4jScriptEngine.java:248)
at org.eclipse.ease.lang.python.py4j.internal.Py4jScriptEngine.execute(Py4jScriptEngine.java:235)
at org.eclipse.ease.AbstractScriptEngine.inject(AbstractScriptEngine.java:190)
at org.eclipse.ease.AbstractScriptEngine.run(AbstractScriptEngine.java:243)
at org.eclipse.core.internal.jobs.Worker.run(Worker.java:63)

Any idea why?

Thanks

Yes: it means that the previous line:
se.get_operational_analysis().get_data_pkg().get_owned_classes()
did not give you any results. Did you create some classes at the operational analysis level in the data package?

My classes are in a data package (folder) in the data package right under “Operational analysis”.

I guess that I would need to recurse for each data package underneath “Operational analysis”.

This doesn’t work (TypeError: ‘DataPkg’ object is not iterable):

myDataPackages = se.get_operational_analysis().get_data_pkg()
for aDataPackage in myDataPackages:
#Get All Classes from the data package in the Operational Analysis
myClasses = aDataPackage.get_owned_classes()

Just for clarification, the code I gave you was a very basic example focused on “I’d like to access the associations” so it did not include how to first navigate all classes.

In your code above: the first datapackage you get is a single object containing the root DataPackage so it is not a list. Hence you cannot iterate on it. Once you’ve got this element, you can navigate in it using get_owned_classes and get_owned_data_pkgs, both giving you lists as results.
An alternative if you want to get all Classes under the root data package:
myDataPackages = se.get_operational_analysis().get_data_pkg()
myClasses = myDataPackages.get_all_contents_by_type(Class)
Once you’ve got that working, there are other parts of my code that won’t work, like the line myAssociation = myAssociations[0] in the case one of your classes does not have any associations leaving from it. So you may have to add some tests on the length of the lists you get.

I hope this helps.
Stephane

Thank you again Stéphane.

I have managed to get the classes, associations and generalizations in my children data packages but I still have these questions for you and the Community:

  1. How to get the name of the (root/child) data package? I have tried .get_name() .getLabel() .get_label()

  2. In the returned associations (.get_contained_properties()), I have only the associations originated (“outgoing”) from the class. I’d like also the “incoming” associations. How to get them?

  3. In my test data, I have only one level of Generalization and in your code, the returned mySuperClass if correct but mySubClass is the class I am looking the specialization/generalization links to/from. In my test data, mySubClass should be “None”, I think.

  4. What are the debugging capabilities for Py4C? I have used PyCharm Community and I am looking for a similar powerful and easy to use debugger for Py4C.

  1. There is a problem with the simplified metamodel specification DataPkg should extend CapellaElement. You can replace:

    class DataPkg(JavaObject):

by

class DataPkg(CapellaElement):

in capella.py. You will be able to use dataPkg.get_name() after this change.

I opened the following issue:

  1. The is no direct reference in the metmaodel to access “incoming” associations but you can use the:

    for element in e_inverse(myClass, “type”):
    if isinstance(java_object, Association):
    #: :type element: Association
    # do something with the association here

Note that Association should also be a CapellaElement.

  1. For the moment the debugger is not working with Python4Capella. We made some changes that might improve the situation but I didn’t test it recently. You can check the current debug documentation and give it a try.

Thank you @YvanLussaud

Any idea regarding Question #3?

Getting this error on this line:
for element in e_inverse(aClassInChildrenPackage, “type”):

org.eclipse.ease.ScriptExecutionException: Traceback (most recent call last):
File “workspace://Python4Capella/MBPC/test.py”, line 41, in
_pyease_string_types = (basestring,)
File “workspace://Python4Capella/java_api/EMF_API.py”, line 36, in e_inverse
from six import integer_types as _pyease_integer_types
File “<…>”, line 57, in eInverse
File “C:\SMW\capella\plugins\py4j-python_0.10.9.5-bnd-2odeag\src\py4j\java_gateway.py”, line 1313, in call
args_command, temp_args = self._build_args(*args)
File “C:\SMW\capella\plugins\py4j-python_0.10.9.5-bnd-2odeag\src\py4j\java_gateway.py”, line 1283, in _build_args
[get_command_part(arg, self.pool) for arg in new_args])
File “C:\SMW\capella\plugins\py4j-python_0.10.9.5-bnd-2odeag\src\py4j\java_gateway.py”, line 1283, in
[get_command_part(arg, self.pool) for arg in new_args])
File “C:\SMW\capella\plugins\py4j-python_0.10.9.5-bnd-2odeag\src\py4j\protocol.py”, line 298, in get_command_part
command_part = REFERENCE_TYPE + parameter._get_object_id()
AttributeError: ‘Class’ object has no attribute ‘_get_object_id’

Added “.get_java_object()”:

  	elements = e_inverse(aClassInChildrenPackage.get_java_object(), "type")
  	print (len(elements))
  	for element in e_inverse(elements, "type"):

The error is gone but there are no elements returned.

Any idea why? Tx

For question 3 I think I’m missing a bit of context and maybe what is the actual question.

and for e_invers() the passed object should be the Java object, my bad. It should look like this:

e_inverse(myClass.get_java_object(), “type”)

This error append when a Python object is passed to a Java method that expect a Java Object (see this page for more details). But in the case of Python4Capella you can get the Java object with get_java_object().

Hi,

I will clarify my question #3, “In my test data, I have only one level of Generalization and in your code, the returned mySuperClass if correct but mySubClass is the class I am looking the specialization/generalization links to/from. In my test data, mySubClass should be “None”, I think”.

In my model: Class A is specialized by Class B
When I apply the code to Class B, mySuperClass = “Class A” which is fine but mySubClass = “Class B” where I would have expected “None”.

I would love also to understand why the e_inverse function is not returning anything:
Added “.get_java_object()”:

  	elements = e_inverse(aClassInChildrenPackage.get_java_object(), "type")
  	print (len(elements))
  	for element in e_inverse(elements, "type"):

Thank you

OK, my mistake; when it prints print("Generalization subClass: " + mySubClass.get_name()), It is actually printing the class name from which the generalization link is leaving, so what I meant is that the Class is the subclass of the SuperClass, not that the code is giving you the subclasses of the current class.

About your question on e_inverse returning nothing: there is no “type” relations between classes in Capella, so the code is giving you nothing. The easiest way for you to navigate in reverse Associations and Generalizations is probably this (it is using the queries from the semantic browser that already calculate the reverse links):

for element in capella_query_by_name(myClass, "Typing Elements"):
    if isinstance(element, Class):
        print(element.get_name())

for element in capella_query_by_name(myClass, "Generalizing Elements"):
    if isinstance(element, Class):
        print(element.get_name())

Stephane Lacrampe
Obeo Canada

Thanks @StephaneLacrampe

From my trials:

  • capella_query_by_name(myClass, "Typing Elements") returns Property objects so the ```
    isinstance(element, Class) is never true

How Typing Elements related to Associations?

Thank you.

“Typing Elements” is the name of a Semantic Browser query. Open the Semantic Browser view, click on one of your Class that is a “SuperClass” in the Project Explorer, and you should see in the Semantic Browser view, on the left part, the list of "Typed Elements on your object.

Very clear and the Semantic Browser is a very useful tool that I am going to use a lot I think.

Still, I don’t understand why none of the returned objects are of Class type but are of Property type. (but I might doing something wrong). Here is the Python function that I wrote based on your inputs:

def getAllClassesAndTheirAssociationsInDataPackageAndBelow(aDataPackage: DataPkg):
    allClassesAndTheirAssociationsInDataPackage = []
    print(aDataPackage.get_name() + " Data Package:")
    #Get All Classes from the data package in the Operational Analysis
    classesInPackage = aDataPackage.get_owned_classes()
    #Get the first Class
    if (classesInPackage != None):
        for aClassInPackage in classesInPackage:
            print("     Found this class: " + aClassInPackage.get_name())
            #Get all properties (associations)
            myAssociations = aClassInPackage.get_contained_properties()
            print (str(len(myAssociations)) + " associations")
            for anAssociation in myAssociations:
                #Get the types of the Property(
                myAssociationTypes = anAssociation.get_type()
                #Get the first type
                myAssociationType = myAssociationTypes[0]
                #Print the name
                print("         The class has outgoing association to: " + myAssociationType.get_name())
            typingElements = capella_query_by_name(aClassInPackage, "Typing Elements")
            print (str(len(typingElements)) + " typing elements")
            for aTypingElement in typingElements:
                print(aTypingElement[0].get_type())
                if isinstance(aTypingElement, Class):
                    print("         The class has incoming association from: " + aTypingElement.get_name())
            #Get all Generalization links
            generalizingElements = capella_query_by_name(aClassInPackage, "Generalizing Elements")
            print (str(len(generalizingElements)) + " generalizing elements")
            for aGeneralizingElement in generalizingElements:
                if isinstance(aGeneralizingElement, Class):
                    print("         The class has incoming association from: " + aGeneralizingElement.get_name())
            #Get all Generalization links
            classGeneralizations = aClassInPackage.get_java_object().getOwnedGeneralizations()
            print (str(len(classGeneralizations)) + " OwnedGeneralizations")
            if (len(classGeneralizations) != 0):
                for aClassGeneralization in classGeneralizations:
                    #Get the source
                    aSubClass = Class(aClassGeneralization.getSub())
                    #Get the target
                    aSuperClass = Class(aClassGeneralization.getSuper())
                    print("         Generalization found: subClass: " + aSubClass.get_name() + " and superClass: " +  aSuperClass.get_name())
                    
        for aChildDataPackage in aDataPackage.get_owned_data_pkgs():
            childrenClassesAndAssociations = []
            childrenClassesAndAssociations = getAllClassesAndTheirAssociationsInDataPackageAndBelow(aChildDataPackage)
            for achildrenClasseAndAssociations in childrenClassesAndAssociations:
                allClassesAndTheirAssociationsInDataPackage.append(achildrenClasseAndAssociations)
    return allClassesAndTheirAssociationsInDataPackage