Metadata-Version: 2.4
Name: aas-submodel-to-py
Version: 0.2.0
Summary: Generate Submodel-Specific classes with filled metainformation derived from submodel templates
Author-email: Igor Garmaev <garmaev@gmx.net>
License: MIT
Project-URL: Homepage, https://github.com/rwth-iat/aas-submodel-template-to-py
Keywords: aas,submodel,class,code,generation,python
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: basyx-python-sdk>=2.1.0
Requires-Dist: jinja2<4,>=3.1.2
Requires-Dist: black
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: mypy; extra == "test"

# aas-submodel-to-py - AAS Submodel Python Code Generator

**aas-submodel-to-py** is a code generator tool built on top of the
[BaSyx-Python-SDK](https://github.com/eclipse-basyx/basyx-python-sdk).
It is designed to generate Submodel-specific classes and classes for its
submodel elements with filled meta-information derived from submodel templates.
These generated classes act as child classes of the BaSyx-Python-SDK classes and
represent classes of the Asset Administration Shell Metamodel.
The hierarchical structure of the generated submodel-specific class includes
all the required submodel element-specific classes. Input files can be `.aasx`,
`.json`, or `.xml` format (see [Supported Input](#supported-input)).

## Examples

### Example of generated classes

Here's a snippet of the classes generated from the
[Digital Nameplate 3.0.1](https://github.com/admin-shell-io/submodel-templates/tree/main/published/Digital%20nameplate/3/0/1)
submodel template (`...` marks omitted lines). Each submodel element gets a class nested in the class of
its submodel or collection. Its init takes the values of the element (e.g. `value`) and its submodel elements;
metadata like `semantic_id` or `description` are keyword arguments, typed by a `TypedDict` per BaSyx class
(e.g. `PropertyMetadata`), and default to the template's values, which `template_defaults()` returns.
The classes build instances (`kind=Instance`), so template qualifiers like `SMT/Cardinality`, which are only
allowed in templates, aren't taken over. Each module starts with a few helpers its classes share:

```python
from typing import Any, Iterable, Optional, Tuple, TypedDict, Union, Unpack, cast
from basyx.aas import model as aas
from basyx.aas.model import datatypes as xsd


def global_ref(value: str) -> aas.ExternalReference:
    """External reference to the global identifier `value`, e.g. a semantic ID"""
    return aas.ExternalReference((aas.Key(aas.KeyTypes.GLOBAL_REFERENCE, value),))


def collect_submodel_elements(*elements: Any, number_repeated: bool = True) -> list[aas.SubmodelElement]:
    """Collect the submodel elements passed to an init in one list: single elements, unless None,
    and the elements of iterables, whose idShorts get an index if `number_repeated` (e.g. Keyword00)"""
    ...


class GeneratedSubmodelElementList(aas.SubmodelElementList):
    """SubmodelElementList, whose items may be instances of subclasses of type_value_list_element,
    such as the generated classes of its items"""
    ...


class MultiLanguagePropertyMetadata(TypedDict, total=False):
    """Metadata of aas.MultiLanguageProperty, passed as keyword arguments to its generated classes"""

    id_short: Optional[str]
    ...
    semantic_id: Optional[aas.Reference]
    ...


class Nameplate(aas.Submodel):

    class URIOfTheProduct(aas.Property):
        ...

    class ManufacturerName(aas.MultiLanguageProperty):

        @staticmethod
        def template_defaults() -> MultiLanguagePropertyMetadata:
            return {
                "id_short": r"ManufacturerName",
                "semantic_id": global_ref(r"0112/2///61987#ABA565#009"),
                "supplemental_semantic_id": (global_ref(r"0173-1#02-AAO677#004"),),
            }

        def __init__(self, value: aas.MultiLanguageTextType, **kwargs: Unpack[MultiLanguagePropertyMetadata]):

            super().__init__(value=value, **(self.template_defaults() | kwargs))

    ...

    class Markings(GeneratedSubmodelElementList):
        ...

    ...

    @staticmethod
    def template_defaults() -> SubmodelMetadata:
        return {
            "id_short": r"Nameplate",
            "description": aas.MultiLanguageTextType(
                dict_={r"en": r"Contains the nameplate information attached to the product"}
            ),
            "administration": aas.AdministrativeInformation(
                version=r"3",
                revision=r"0",
                ...
                template_id=r"https://admin-shell.io/idta-02006-3-0",
                ...
            ),
            "semantic_id": global_ref(r"https://admin-shell.io/idta/nameplate/3/0/Nameplate"),
        }

    def __init__(
        self,
        id_: str,
        uRIOfTheProduct: Union[xsd.AnyURI, URIOfTheProduct],
        manufacturerName: Union[aas.MultiLanguageTextType, ManufacturerName],
        ...
        yearOfConstruction: Optional[Union[str, YearOfConstruction]] = None,
        ...
        markings: Optional[Union[Iterable[Markings.Markings_item], Markings]] = None,
        ...
        **kwargs: Unpack[SubmodelMetadata],
    ):
        ...
```

### Usage Example of the generated class

Here's an example of instantiating the generated `Nameplate` submodel. The classes generated from
Digital Nameplate 3.0.1 are part of [py-aas-submodels](../py-aas-submodels/README.md):

```python
from basyx.aas.model import MultiLanguageTextType
from py_aas_submodels.digital_nameplate_3_0_1 import Nameplate

nameplate = Nameplate(
    id_="https://example.com/ids/sm/nameplate-001",
    uRIOfTheProduct="https://www.domain-abc.com/Model-Nr-1234",
    manufacturerName=MultiLanguageTextType({"de": "Muster AG"}),
    manufacturerProductDesignation=MultiLanguageTextType({"en": "ABC-123"}),
    addressInformation=Nameplate.AddressInformation(),
    orderCodeOfManufacturer="ABC-123-XYZ",
    yearOfConstruction="2022",
    serialNumber=Nameplate.SerialNumber("12345678"),
)
```

All required submodel elements are positional arguments; optional elements default to `None`.
Metadata is passed as keyword arguments, e.g. `Nameplate(..., id_short="Nameplate01")`, and replaces the
template's value (`None` removes it); type checkers like mypy report unknown or wrongly typed metadata.
Instead of submodel elements (e.g. `Nameplate.SerialNumber("12345678")`), raw values can be passed
(e.g. `yearOfConstruction="2022"`); multi-language values are passed as `MultiLanguageTextType`.
Properties with a default value in the template (`SMT/DefaultValue`, else `SMT/InitialValue`) can be
created without value, e.g. `PAMSpecificationSheet.GeneralInformation.SafetyMeasure()`.
Of elements marked as alternatives (`SMT/EitherOr`), only one may be given; passing more raises a `ValueError`.
Values restricted by the template (`SMT/AllowedValue`, `SMT/AllowedRange`, `FormChoices`, `enumeration`) are
checked as well and available as class attributes, e.g. `SafetyMeasure.ALLOWED_VALUES == ("True", "False")`.
Lists (`SubmodelElementList`) take an iterable of their items or raw values, e.g. `phases=["A1", "B2"]`.
List items have no idShort by default, as it is optional since AAS metamodel 3.1.
The output is auto-formatted with Black and passes the type checker mypy.

## Installation

Using PyPI:

```bash
pip install aas-submodel-to-py
```

Or from the repository root:

```bash
pip install -e ./aas-submodel-to-py
```

## Supported Input

| Format | AAS metamodel 3.1 | AAS metamodel 3.0 |
|---|---|---|
| `.json` | ✅ | ✅ |
| `.aasx` | ✅ | ❌ |
| `.xml` | ✅ | ❌ |

Files are read with the BaSyx Python SDK (≥ 2.1.0), which implements AAS metamodel 3.1 and can't
read AASX and XML files of metamodel 3.0. Many templates on the IDTA website are still such files.
For those, use the JSON version of the template, or its `_forAASMetamodelV3.1` variant; both are
published in [admin-shell-io/submodel-templates](https://github.com/admin-shell-io/submodel-templates/tree/main/published).

For such files, as for any file without a submodel, the generator raises a `NoSubmodelError`
(`submodel_to_code` exits with an error message) instead of writing a module.

## Usage

### Command Line

```bash
# Minimal: output .py is generated next to the input file
# e.g. IDTA-02006-2-0_Submodel_Digital Nameplate.aasx → IDTA_02006_2_0_Submodel_Digital_Nameplate.py
submodel_to_code -i /some/path/DigitalNameplate.aasx

# Write to a specific output directory (filename still auto-derived)
submodel_to_code -i /some/path/DigitalNameplate.aasx -d /some/other/path/

# Write to an explicit output file path
submodel_to_code -i /some/path/DigitalNameplate.aasx -o /some/path/output.py

# Overwrite an existing output file
submodel_to_code -i /some/path/DigitalNameplate.aasx --force
```

| Flag | Long form | Description | Required |
|---|---|---|---|
| `-i` | `--aas_path` | Input AAS file (`.aasx`, `.json`, or `.xml`) | Yes |
| `-o` | `--outpath` | Output `.py` file path (mutually exclusive with `-d`) | No |
| `-d` | `--outdir` | Output directory; filename is derived from the input filename (mutually exclusive with `-o`) | No |
| `-f` | `--force` | Overwrite the output file if it already exists | No |

If neither `-o` nor `-d` is given, the output file is written next to the input file with hyphens and spaces in the name replaced by underscores.

If the entry-point is not on PATH, use the module invocation:

```bash
python -m aas_submodel_to_py.submodel_to_code \
    -i /some/path/DigitalNameplate.aasx \
    -o /some/path/output.py
```

### Python API

```python
from aas_submodel_to_py import SubmodelCodegen

codegen = SubmodelCodegen()

# Generate from an AAS file (.aasx, .json, or .xml)
codegen.generate_from(
    input_file="/some/path/DigitalNameplate.aasx",
    output_file="nameplate.py"
)

# If you already have an AAS object store loaded in memory
codegen.generate_from_obj_store(
    obj_store=my_existing_store,
    output_file="output.py"
)
```

## Running Tests

From the repository root:

```bash
pip install -e "./aas-submodel-to-py[test]"
python -m pytest aas-submodel-to-py/tests
```

The [Tests workflow](../.github/workflows/tests.yml) runs these tests, and the import checks of all
modules in [py-aas-submodels](../py-aas-submodels/README.md#running-tests), on Python 3.11 and 3.14
for every push to `master`/`develop` and every pull request.

## Support and Contribution

If you encounter any issues, or want to contribute to the project, feel free to open an issue or a pull request. Your
contributions are always welcome!

## License

This project is licensed under the MIT License. See the LICENSE file for details.
