diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 2f147c17..715769f2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,9 @@ on:
release:
types: [published]
+env:
+ TARGET_PLATFORM: "linux/amd64,linux/arm/v7,linux/arm64"
+
jobs:
sdk-publish:
# This job publishes the package to PyPI
@@ -59,7 +62,54 @@ jobs:
with:
password: ${{ secrets.PYPI_ORG_TOKEN }}
- server-publish:
+ server-repository-publish:
+ # This job publishes the server docker image to DockerHub
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./server
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Extract Docker image metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: eclipsebasyx/basyx-python-repository
+ # This fetches the latest git tag and adds an additional "latest" to it, so e.g. `2.1.0,latest`
+ tags: |
+ type=semver,pattern={{version}}
+ type=raw,value=latest
+ labels: |
+ org.opencontainers.image.title=BaSyx Python Repository Service
+ org.opencontainers.image.description=Eclipse BaSyx Python SDK - Repository HTTP Server
+ org.opencontainers.image.source=https://github.com/eclipse-basyx/basyx-python-sdk/tree/main/server
+ org.opencontainers.image.licenses=MIT
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKER_HUB_USER }}
+ password: ${{ secrets.DOCKER_HUB_TOKEN }}
+
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and Push Repository Docker Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./server/docker/repository/Dockerfile
+ platforms: ${{ env.TARGET_PLATFORM }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ server-discovery-publish:
# This job publishes the server docker image to DockerHub
runs-on: ubuntu-latest
defaults:
@@ -73,14 +123,14 @@ jobs:
id: meta
uses: docker/metadata-action@v5
with:
- images: eclipsebasyx/basyx-python-server
+ images: eclipsebasyx/basyx-python-discovery
# This fetches the latest git tag and adds an additional "latest" to it, so e.g. `2.1.0,latest`
tags: |
type=semver,pattern={{version}}
type=raw,value=latest
labels: |
- org.opencontainers.image.title=BaSyx Python Server
- org.opencontainers.image.description=Eclipse BaSyx Python SDK - HTTP Server
+ org.opencontainers.image.title=BaSyx Python Discovery Service
+ org.opencontainers.image.description=Eclipse BaSyx Python SDK - Discovery HTTP Server
org.opencontainers.image.source=https://github.com/eclipse-basyx/basyx-python-sdk/tree/main/server
org.opencontainers.image.licenses=MIT
@@ -90,11 +140,65 @@ jobs:
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- - name: Build and Push Docker Image
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and Push Repository Docker Image
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ./server/docker/discovery/Dockerfile
+ platforms: ${{ env.TARGET_PLATFORM }}
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ server-registry-publish:
+ # This job publishes the server docker image to DockerHub
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: ./server
+ steps:
+ - name: Checkout Repository
+ uses: actions/checkout@v4
+
+ - name: Extract Docker image metadata
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: eclipsebasyx/basyx-python-registry
+ # This fetches the latest git tag and adds an additional "latest" to it, so e.g. `2.1.0,latest`
+ tags: |
+ type=semver,pattern={{version}}
+ type=raw,value=latest
+ labels: |
+ org.opencontainers.image.title=BaSyx Python Registry Service
+ org.opencontainers.image.description=Eclipse BaSyx Python SDK - Registry HTTP Server
+ org.opencontainers.image.source=https://github.com/eclipse-basyx/basyx-python-sdk/tree/main/server
+ org.opencontainers.image.licenses=MIT
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v4
+ with:
+ username: ${{ secrets.DOCKER_HUB_USER }}
+ password: ${{ secrets.DOCKER_HUB_TOKEN }}
+
+ - name: Setup QEMU
+ uses: docker/setup-qemu-action@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v4
+
+ - name: Build and Push Repository Docker Image
uses: docker/build-push-action@v6
with:
context: .
- file: ./server/Dockerfile # Todo: Update paths
+ file: ./server/docker/registry/Dockerfile
+ platforms: ${{ env.TARGET_PLATFORM }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
diff --git a/compliance_tool/aas_compliance_tool/compliance_check_aasx.py b/compliance_tool/aas_compliance_tool/compliance_check_aasx.py
index 0b10f5fe..40c4f2cd 100644
--- a/compliance_tool/aas_compliance_tool/compliance_check_aasx.py
+++ b/compliance_tool/aas_compliance_tool/compliance_check_aasx.py
@@ -88,69 +88,6 @@ def check_deserialization(file_path: str, state_manager: ComplianceToolStateMana
return identifiable_store, files, new_cp
-def check_schema(file_path: str, state_manager: ComplianceToolStateManager) -> None:
- """
- Checks a given file against the official json schema and reports any issues using the given
- :class:`~basyx.aas.compliance_tool.state_manager.ComplianceToolStateManager`
-
- Opens the file and checks if the data inside is stored in XML or JSON. Then calls the respective compliance tool
- schema check
- """
- logger = logging.getLogger('compliance_check')
- logger.addHandler(state_manager)
- logger.propagate = False
- logger.setLevel(logging.INFO)
-
- # create handler to get logger info
- logger_deserialization = logging.getLogger(aasx.__name__)
- logger_deserialization.addHandler(state_manager)
- logger_deserialization.propagate = False
- logger_deserialization.setLevel(logging.INFO)
-
- state_manager.add_step('Open file')
- try:
- # open given file
- reader = aasx.AASXReader(file_path)
- state_manager.set_step_status_from_log()
- except ValueError as error:
- logger.error(error)
- state_manager.set_step_status_from_log()
- state_manager.add_step('Read file')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- return
-
- try:
- # read given file (Find XML and JSON parts)
- state_manager.add_step('Read file')
- core_rels = reader.reader.get_related_parts_by_type()
- try:
- aasx_origin_part = core_rels[aasx.RELATIONSHIP_TYPE_AASX_ORIGIN][0]
- except IndexError as e:
- raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e
- state_manager.set_step_status(Status.SUCCESS)
- for aas_part in reader.reader.get_related_parts_by_type(aasx_origin_part)[
- aasx.RELATIONSHIP_TYPE_AAS_SPEC]:
- content_type = reader.reader.get_content_type(aas_part)
- extension = aas_part.split("/")[-1].split(".")[-1]
- with reader.reader.open_part(aas_part) as p:
- if content_type.split(";")[0] in (
- "text/xml", "application/xml") or content_type == "" and extension == "xml":
- logger.debug("Parsing AAS objects from XML stream in OPC part {} ...".format(aas_part))
- compliance_check_xml._check_schema(p, state_manager)
- elif content_type.split(";")[0] == "application/json" \
- or content_type == "" and extension == "json":
- logger.debug("Parsing AAS objects from JSON stream in OPC part {} ...".format(aas_part))
- compliance_check_json._check_schema(io.TextIOWrapper(p, encoding='utf-8-sig'), state_manager)
- else:
- raise ValueError("Could not determine part format of AASX part {} (Content Type: {}, extension: {}"
- .format(aas_part, content_type, extension))
- except ValueError as error:
- logger.error(error)
- state_manager.set_step_status(Status.FAILED)
- finally:
- reader.close()
-
-
def check_aas_example(file_path: str, state_manager: ComplianceToolStateManager, **kwargs) -> None:
"""
Checks if a file contains all elements of the aas example and reports any issues using the given
diff --git a/compliance_tool/aas_compliance_tool/compliance_check_json.py b/compliance_tool/aas_compliance_tool/compliance_check_json.py
index b021fa96..e50332ec 100644
--- a/compliance_tool/aas_compliance_tool/compliance_check_json.py
+++ b/compliance_tool/aas_compliance_tool/compliance_check_json.py
@@ -23,84 +23,6 @@
from aas_compliance_tool.state_manager import ComplianceToolStateManager, Status
-JSON_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'schemas/aasJSONSchema.json')
-
-
-def check_schema(file_path: str, state_manager: ComplianceToolStateManager) -> None:
- """
- Checks a given file against the official json schema and reports any issues using the given
- :class:`~basyx.aas.compliance_tool.state_manager.ComplianceToolStateManager`
-
- Add the steps: `Open file`, `Read file and check if it is conform to the json syntax` and `Validate file against
- official json schema`
-
- :param file_path: Path to the file which should be checked
- :param state_manager: :class:`~basyx.aas.compliance_tool.state_manager.ComplianceToolStateManager` to log the steps
- """
- logger = logging.getLogger('compliance_check')
- logger.addHandler(state_manager)
- logger.propagate = False
- logger.setLevel(logging.INFO)
-
- state_manager.add_step('Open file')
- try:
- # open given file
- file_to_be_checked = open(file_path, 'r', encoding='utf-8-sig')
- except IOError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- state_manager.add_step('Read file and check if it is conform to the json syntax')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- state_manager.add_step('Validate file against official json schema')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- return
- return _check_schema(file_to_be_checked, state_manager)
-
-
-def _check_schema(file_to_be_checked: IO[str], state_manager: ComplianceToolStateManager):
- logger = logging.getLogger('compliance_check')
- logger.addHandler(state_manager)
- logger.propagate = False
- logger.setLevel(logging.INFO)
-
- try:
- with file_to_be_checked:
- state_manager.set_step_status(Status.SUCCESS)
- # read given file and check if it is conform to the json syntax
- state_manager.add_step('Read file and check if it is conform to the json syntax')
- json_to_be_checked = json.load(file_to_be_checked)
- state_manager.set_step_status(Status.SUCCESS)
- except json.decoder.JSONDecodeError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- state_manager.add_step('Validate file against official json schema')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- return
-
- # load json schema
- with open(JSON_SCHEMA_FILE, 'r', encoding='utf-8-sig') as json_file:
- aas_json_schema = json.load(json_file)
- state_manager.add_step('Validate file against official json schema')
-
- # validate given file against schema
- try:
- import jsonschema # type: ignore
- except ImportError as error:
- state_manager.set_step_status(Status.NOT_EXECUTED)
- logger.error("Python package 'jsonschema' is required for validating the JSON file.", error)
- return
-
- try:
- jsonschema.validate(instance=json_to_be_checked, schema=aas_json_schema)
- except jsonschema.exceptions.ValidationError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- return
-
- state_manager.set_step_status(Status.SUCCESS)
- return
-
-
def check_deserialization(file_path: str, state_manager: ComplianceToolStateManager,
file_info: Optional[str] = None) -> model.DictIdentifiableStore:
"""
diff --git a/compliance_tool/aas_compliance_tool/compliance_check_xml.py b/compliance_tool/aas_compliance_tool/compliance_check_xml.py
index 81f2b5ff..eeb9924c 100644
--- a/compliance_tool/aas_compliance_tool/compliance_check_xml.py
+++ b/compliance_tool/aas_compliance_tool/compliance_check_xml.py
@@ -23,83 +23,6 @@
from aas_compliance_tool.state_manager import ComplianceToolStateManager, Status
-XML_SCHEMA_FILE = os.path.join(os.path.dirname(__file__), 'schemas/aasXMLSchema.xsd')
-
-
-def check_schema(file_path: str, state_manager: ComplianceToolStateManager) -> None:
- """
- Checks a given file against the official xml schema and reports any issues using the given
- :class:`~basyx.aas.compliance_tool.state_manager.ComplianceToolStateManager`
-
- Add the steps: `Open file`, `Read file`, `Check if it is conform to the xml syntax` and `Validate file against
- official xml schema`
-
- :param file_path: Path to the file which should be checked
- :param state_manager: :class:`~basyx.aas.compliance_tool.state_manager.ComplianceToolStateManager` to log the steps
- """
- logger = logging.getLogger('compliance_check')
- logger.addHandler(state_manager)
- logger.propagate = False
- logger.setLevel(logging.INFO)
-
- state_manager.add_step('Open file')
- try:
- # open given file
- file_to_be_checked = open(file_path, 'rb')
- state_manager.set_step_status(Status.SUCCESS)
- except IOError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- state_manager.add_step('Read file and check if it is conform to the xml syntax')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- state_manager.add_step('Validate file against official xml schema')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- return
- return _check_schema(file_to_be_checked, state_manager)
-
-
-def _check_schema(file_to_be_checked, state_manager):
- logger = logging.getLogger('compliance_check')
- logger.addHandler(state_manager)
- logger.propagate = False
- logger.setLevel(logging.INFO)
-
- state_manager.add_step('Read file and check if it is conform to the xml syntax')
- try:
- # read given file and check if it is conform to the xml syntax
- parser = etree.XMLParser(remove_blank_text=True, remove_comments=True)
- etree.parse(file_to_be_checked, parser)
- state_manager.set_step_status(Status.SUCCESS)
- except etree.XMLSyntaxError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- state_manager.add_step('Validate file against official xml schema')
- state_manager.set_step_status(Status.NOT_EXECUTED)
- file_to_be_checked.close()
- return
- except Exception:
- file_to_be_checked.close()
- raise
-
- # load aas xml schema
- aas_xml_schema = etree.XMLSchema(file=XML_SCHEMA_FILE)
- parser = etree.XMLParser(schema=aas_xml_schema)
-
- state_manager.add_step('Validate file against official xml schema')
- # validate given file against schema
- try:
- file_to_be_checked.seek(0) # Reset reading file offset (cursor) to the beginning of the file
- with file_to_be_checked:
- etree.parse(file_to_be_checked, parser=parser)
- except etree.ParseError as error:
- state_manager.set_step_status(Status.FAILED)
- logger.error(error)
- return
-
- state_manager.set_step_status(Status.SUCCESS)
- return
-
-
def check_deserialization(file_path: str, state_manager: ComplianceToolStateManager,
file_info: Optional[str] = None) -> model.DictIdentifiableStore:
"""
diff --git a/compliance_tool/aas_compliance_tool/schemas/aasJSONSchema.json b/compliance_tool/aas_compliance_tool/schemas/aasJSONSchema.json
deleted file mode 100644
index 7ba1a360..00000000
--- a/compliance_tool/aas_compliance_tool/schemas/aasJSONSchema.json
+++ /dev/null
@@ -1,1528 +0,0 @@
-{
- "$schema": "https://json-schema.org/draft/2019-09/schema",
- "title": "AssetAdministrationShellEnvironment",
- "type": "object",
- "allOf": [
- {
- "$ref": "#/definitions/Environment"
- }
- ],
- "$id": "https://admin-shell.io/aas/3/1",
- "definitions": {
- "AasSubmodelElements": {
- "type": "string",
- "enum": [
- "AnnotatedRelationshipElement",
- "BasicEventElement",
- "Blob",
- "Capability",
- "DataElement",
- "Entity",
- "EventElement",
- "File",
- "MultiLanguageProperty",
- "Operation",
- "Property",
- "Range",
- "ReferenceElement",
- "RelationshipElement",
- "SubmodelElement",
- "SubmodelElementCollection",
- "SubmodelElementList"
- ]
- },
- "AbstractLangString": {
- "type": "object",
- "properties": {
- "language": {
- "type": "string",
- "pattern": "^(([a-zA-Z]{2,3}(-[a-zA-Z]{3}(-[a-zA-Z]{3}){2})?|[a-zA-Z]{4}|[a-zA-Z]{5,8})(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-(([a-zA-Z0-9]){5,8}|[0-9]([a-zA-Z0-9]){3}))*(-[0-9A-WY-Za-wy-z](-([a-zA-Z0-9]){2,8})+)*(-[xX](-([a-zA-Z0-9]){1,8})+)?|[xX](-([a-zA-Z0-9]){1,8})+|((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang)))$"
- },
- "text": {
- "type": "string",
- "minLength": 1,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- }
- },
- "required": [
- "language",
- "text"
- ]
- },
- "AdministrativeInformation": {
- "allOf": [
- {
- "$ref": "#/definitions/HasDataSpecification"
- },
- {
- "properties": {
- "version": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 4
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^(0|[1-9][0-9]*)$"
- }
- ]
- },
- "revision": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 4
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^(0|[1-9][0-9]*)$"
- }
- ]
- },
- "creator": {
- "$ref": "#/definitions/Reference"
- },
- "templateId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- }
- }
- }
- ]
- },
- "AnnotatedRelationshipElement": {
- "allOf": [
- {
- "$ref": "#/definitions/RelationshipElement_abstract"
- },
- {
- "properties": {
- "annotations": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/DataElement_choice"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "AnnotatedRelationshipElement"
- }
- }
- }
- ]
- },
- "AssetAdministrationShell": {
- "allOf": [
- {
- "$ref": "#/definitions/Identifiable"
- },
- {
- "$ref": "#/definitions/HasDataSpecification"
- },
- {
- "properties": {
- "derivedFrom": {
- "$ref": "#/definitions/Reference"
- },
- "assetInformation": {
- "$ref": "#/definitions/AssetInformation"
- },
- "submodels": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Reference"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "AssetAdministrationShell"
- }
- },
- "required": [
- "assetInformation"
- ]
- }
- ]
- },
- "AssetInformation": {
- "type": "object",
- "properties": {
- "assetKind": {
- "$ref": "#/definitions/AssetKind"
- },
- "globalAssetId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "specificAssetIds": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SpecificAssetId"
- },
- "minItems": 1
- },
- "assetType": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "defaultThumbnail": {
- "$ref": "#/definitions/Resource"
- }
- },
- "required": [
- "assetKind"
- ]
- },
- "AssetKind": {
- "type": "string",
- "enum": [
- "Instance",
- "NotApplicable",
- "Type"
- ]
- },
- "BasicEventElement": {
- "allOf": [
- {
- "$ref": "#/definitions/EventElement"
- },
- {
- "properties": {
- "observed": {
- "$ref": "#/definitions/Reference"
- },
- "direction": {
- "$ref": "#/definitions/Direction"
- },
- "state": {
- "$ref": "#/definitions/StateOfEvent"
- },
- "messageTopic": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "messageBroker": {
- "$ref": "#/definitions/Reference"
- },
- "lastUpdate": {
- "type": "string",
- "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$"
- },
- "minInterval": {
- "type": "string",
- "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$"
- },
- "maxInterval": {
- "type": "string",
- "pattern": "^-?P((([0-9]+Y([0-9]+M)?([0-9]+D)?|([0-9]+M)([0-9]+D)?|([0-9]+D))(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S)))?)|(T(([0-9]+H)([0-9]+M)?([0-9]+(\\.[0-9]+)?S)?|([0-9]+M)([0-9]+(\\.[0-9]+)?S)?|([0-9]+(\\.[0-9]+)?S))))$"
- },
- "modelType": {
- "const": "BasicEventElement"
- }
- },
- "required": [
- "observed",
- "direction",
- "state"
- ]
- }
- ]
- },
- "Blob": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "value": {
- "type": "string",
- "contentEncoding": "base64"
- },
- "contentType": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 100
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$"
- }
- ]
- },
- "modelType": {
- "const": "Blob"
- }
- },
- "required": [
- "contentType"
- ]
- }
- ]
- },
- "Capability": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "modelType": {
- "const": "Capability"
- }
- }
- }
- ]
- },
- "ConceptDescription": {
- "allOf": [
- {
- "$ref": "#/definitions/Identifiable"
- },
- {
- "$ref": "#/definitions/HasDataSpecification"
- },
- {
- "properties": {
- "isCaseOf": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Reference"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "ConceptDescription"
- }
- }
- }
- ]
- },
- "DataElement": {
- "$ref": "#/definitions/SubmodelElement"
- },
- "DataElement_choice": {
- "oneOf": [
- {
- "$ref": "#/definitions/Blob"
- },
- {
- "$ref": "#/definitions/File"
- },
- {
- "$ref": "#/definitions/MultiLanguageProperty"
- },
- {
- "$ref": "#/definitions/Property"
- },
- {
- "$ref": "#/definitions/Range"
- },
- {
- "$ref": "#/definitions/ReferenceElement"
- }
- ]
- },
- "DataSpecificationContent": {
- "type": "object",
- "properties": {
- "modelType": {
- "$ref": "#/definitions/ModelType"
- }
- },
- "required": [
- "modelType"
- ]
- },
- "DataSpecificationContent_choice": {
- "oneOf": [
- {
- "$ref": "#/definitions/DataSpecificationIec61360"
- }
- ]
- },
- "DataSpecificationIec61360": {
- "allOf": [
- {
- "$ref": "#/definitions/DataSpecificationContent"
- },
- {
- "properties": {
- "preferredName": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringPreferredNameTypeIec61360"
- },
- "minItems": 1
- },
- "shortName": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringShortNameTypeIec61360"
- },
- "minItems": 1
- },
- "unit": {
- "type": "string",
- "minLength": 1,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "unitId": {
- "$ref": "#/definitions/Reference"
- },
- "sourceOfDefinition": {
- "type": "string",
- "minLength": 1,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "symbol": {
- "type": "string",
- "minLength": 1,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "dataType": {
- "$ref": "#/definitions/DataTypeIec61360"
- },
- "definition": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringDefinitionTypeIec61360"
- },
- "minItems": 1
- },
- "valueFormat": {
- "type": "string",
- "minLength": 1,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "valueList": {
- "$ref": "#/definitions/ValueList"
- },
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "levelType": {
- "$ref": "#/definitions/LevelType"
- },
- "modelType": {
- "const": "DataSpecificationIec61360"
- }
- },
- "required": [
- "preferredName"
- ]
- }
- ]
- },
- "DataTypeDefXsd": {
- "type": "string",
- "enum": [
- "xs:anyURI",
- "xs:base64Binary",
- "xs:boolean",
- "xs:byte",
- "xs:date",
- "xs:dateTime",
- "xs:decimal",
- "xs:double",
- "xs:duration",
- "xs:float",
- "xs:gDay",
- "xs:gMonth",
- "xs:gMonthDay",
- "xs:gYear",
- "xs:gYearMonth",
- "xs:hexBinary",
- "xs:int",
- "xs:integer",
- "xs:long",
- "xs:negativeInteger",
- "xs:nonNegativeInteger",
- "xs:nonPositiveInteger",
- "xs:positiveInteger",
- "xs:short",
- "xs:string",
- "xs:time",
- "xs:unsignedByte",
- "xs:unsignedInt",
- "xs:unsignedLong",
- "xs:unsignedShort"
- ]
- },
- "DataTypeIec61360": {
- "type": "string",
- "enum": [
- "BLOB",
- "BOOLEAN",
- "DATE",
- "FILE",
- "HTML",
- "INTEGER_COUNT",
- "INTEGER_CURRENCY",
- "INTEGER_MEASURE",
- "IRDI",
- "IRI",
- "RATIONAL",
- "RATIONAL_MEASURE",
- "REAL_COUNT",
- "REAL_CURRENCY",
- "REAL_MEASURE",
- "STRING",
- "STRING_TRANSLATABLE",
- "TIME",
- "TIMESTAMP"
- ]
- },
- "Direction": {
- "type": "string",
- "enum": [
- "input",
- "output"
- ]
- },
- "EmbeddedDataSpecification": {
- "type": "object",
- "properties": {
- "dataSpecification": {
- "$ref": "#/definitions/Reference"
- },
- "dataSpecificationContent": {
- "$ref": "#/definitions/DataSpecificationContent_choice"
- }
- },
- "required": [
- "dataSpecification",
- "dataSpecificationContent"
- ]
- },
- "Entity": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "statements": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SubmodelElement_choice"
- },
- "minItems": 1
- },
- "entityType": {
- "$ref": "#/definitions/EntityType"
- },
- "globalAssetId": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "specificAssetIds": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SpecificAssetId"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "Entity"
- }
- },
- "required": [
- "entityType"
- ]
- }
- ]
- },
- "EntityType": {
- "type": "string",
- "enum": [
- "CoManagedEntity",
- "SelfManagedEntity"
- ]
- },
- "Environment": {
- "type": "object",
- "properties": {
- "assetAdministrationShells": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/AssetAdministrationShell"
- },
- "minItems": 1
- },
- "submodels": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Submodel"
- },
- "minItems": 1
- },
- "conceptDescriptions": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/ConceptDescription"
- },
- "minItems": 1
- }
- }
- },
- "EventElement": {
- "$ref": "#/definitions/SubmodelElement"
- },
- "EventPayload": {
- "type": "object",
- "properties": {
- "source": {
- "$ref": "#/definitions/Reference"
- },
- "sourceSemanticId": {
- "$ref": "#/definitions/Reference"
- },
- "observableReference": {
- "$ref": "#/definitions/Reference"
- },
- "observableSemanticId": {
- "$ref": "#/definitions/Reference"
- },
- "topic": {
- "type": "string",
- "minLength": 1,
- "maxLength": 255,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "subjectId": {
- "$ref": "#/definitions/Reference"
- },
- "timeStamp": {
- "type": "string",
- "pattern": "^-?(([1-9][0-9][0-9][0-9]+)|(0[0-9][0-9][0-9]))-((0[1-9])|(1[0-2]))-((0[1-9])|([12][0-9])|(3[01]))T(((([01][0-9])|(2[0-3])):[0-5][0-9]:([0-5][0-9])(\\.[0-9]+)?)|24:00:00(\\.0+)?)(Z|\\+00:00|-00:00)$"
- },
- "payload": {
- "type": "string",
- "contentEncoding": "base64"
- }
- },
- "required": [
- "source",
- "observableReference",
- "timeStamp"
- ]
- },
- "Extension": {
- "allOf": [
- {
- "$ref": "#/definitions/HasSemantics"
- },
- {
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "valueType": {
- "$ref": "#/definitions/DataTypeDefXsd"
- },
- "value": {
- "type": "string"
- },
- "refersTo": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Reference"
- },
- "minItems": 1
- }
- },
- "required": [
- "name"
- ]
- }
- ]
- },
- "File": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "value": {
- "type": "string"
- },
- "contentType": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 100
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$"
- }
- ]
- },
- "modelType": {
- "const": "File"
- }
- },
- "required": [
- "contentType"
- ]
- }
- ]
- },
- "HasDataSpecification": {
- "type": "object",
- "properties": {
- "embeddedDataSpecifications": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/EmbeddedDataSpecification"
- },
- "minItems": 1
- }
- }
- },
- "HasExtensions": {
- "type": "object",
- "properties": {
- "extensions": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Extension"
- },
- "minItems": 1
- }
- }
- },
- "HasKind": {
- "type": "object",
- "properties": {
- "kind": {
- "$ref": "#/definitions/ModellingKind"
- }
- }
- },
- "HasSemantics": {
- "type": "object",
- "properties": {
- "semanticId": {
- "$ref": "#/definitions/Reference"
- },
- "supplementalSemanticIds": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Reference"
- },
- "minItems": 1
- }
- }
- },
- "Identifiable": {
- "allOf": [
- {
- "$ref": "#/definitions/Referable"
- },
- {
- "properties": {
- "administration": {
- "$ref": "#/definitions/AdministrativeInformation"
- },
- "id": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- }
- },
- "required": [
- "id"
- ]
- }
- ]
- },
- "Key": {
- "type": "object",
- "properties": {
- "type": {
- "$ref": "#/definitions/KeyTypes"
- },
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- }
- },
- "required": [
- "type",
- "value"
- ]
- },
- "KeyTypes": {
- "type": "string",
- "enum": [
- "AnnotatedRelationshipElement",
- "AssetAdministrationShell",
- "BasicEventElement",
- "Blob",
- "Capability",
- "ConceptDescription",
- "DataElement",
- "Entity",
- "EventElement",
- "File",
- "FragmentReference",
- "GlobalReference",
- "Identifiable",
- "MultiLanguageProperty",
- "Operation",
- "Property",
- "Range",
- "Referable",
- "ReferenceElement",
- "RelationshipElement",
- "Submodel",
- "SubmodelElement",
- "SubmodelElementCollection",
- "SubmodelElementList"
- ]
- },
- "LangStringDefinitionTypeIec61360": {
- "allOf": [
- {
- "$ref": "#/definitions/AbstractLangString"
- },
- {
- "properties": {
- "text": {
- "maxLength": 1023
- }
- }
- }
- ]
- },
- "LangStringNameType": {
- "allOf": [
- {
- "$ref": "#/definitions/AbstractLangString"
- },
- {
- "properties": {
- "text": {
- "maxLength": 128
- }
- }
- }
- ]
- },
- "LangStringPreferredNameTypeIec61360": {
- "allOf": [
- {
- "$ref": "#/definitions/AbstractLangString"
- },
- {
- "properties": {
- "text": {
- "maxLength": 255
- }
- }
- }
- ]
- },
- "LangStringShortNameTypeIec61360": {
- "allOf": [
- {
- "$ref": "#/definitions/AbstractLangString"
- },
- {
- "properties": {
- "text": {
- "maxLength": 18
- }
- }
- }
- ]
- },
- "LangStringTextType": {
- "allOf": [
- {
- "$ref": "#/definitions/AbstractLangString"
- },
- {
- "properties": {
- "text": {
- "maxLength": 1023
- }
- }
- }
- ]
- },
- "LevelType": {
- "type": "object",
- "properties": {
- "min": {
- "type": "boolean"
- },
- "nom": {
- "type": "boolean"
- },
- "typ": {
- "type": "boolean"
- },
- "max": {
- "type": "boolean"
- }
- },
- "required": [
- "min",
- "nom",
- "typ",
- "max"
- ]
- },
- "ModelType": {
- "type": "string",
- "enum": [
- "AnnotatedRelationshipElement",
- "AssetAdministrationShell",
- "BasicEventElement",
- "Blob",
- "Capability",
- "ConceptDescription",
- "DataSpecificationIec61360",
- "Entity",
- "File",
- "MultiLanguageProperty",
- "Operation",
- "Property",
- "Range",
- "ReferenceElement",
- "RelationshipElement",
- "Submodel",
- "SubmodelElementCollection",
- "SubmodelElementList"
- ]
- },
- "ModellingKind": {
- "type": "string",
- "enum": [
- "Instance",
- "Template"
- ]
- },
- "MultiLanguageProperty": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "value": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringTextType"
- },
- "minItems": 1
- },
- "valueId": {
- "$ref": "#/definitions/Reference"
- },
- "modelType": {
- "const": "MultiLanguageProperty"
- }
- }
- }
- ]
- },
- "Operation": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "inputVariables": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/OperationVariable"
- },
- "minItems": 1
- },
- "outputVariables": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/OperationVariable"
- },
- "minItems": 1
- },
- "inoutputVariables": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/OperationVariable"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "Operation"
- }
- }
- }
- ]
- },
- "OperationVariable": {
- "type": "object",
- "properties": {
- "value": {
- "$ref": "#/definitions/SubmodelElement_choice"
- }
- },
- "required": [
- "value"
- ]
- },
- "Property": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "valueType": {
- "$ref": "#/definitions/DataTypeDefXsd"
- },
- "value": {
- "type": "string"
- },
- "valueId": {
- "$ref": "#/definitions/Reference"
- },
- "modelType": {
- "const": "Property"
- }
- },
- "required": [
- "valueType"
- ]
- }
- ]
- },
- "Qualifiable": {
- "type": "object",
- "properties": {
- "qualifiers": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Qualifier"
- },
- "minItems": 1
- },
- "modelType": {
- "$ref": "#/definitions/ModelType"
- }
- },
- "required": [
- "modelType"
- ]
- },
- "Qualifier": {
- "allOf": [
- {
- "$ref": "#/definitions/HasSemantics"
- },
- {
- "properties": {
- "kind": {
- "$ref": "#/definitions/QualifierKind"
- },
- "type": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "valueType": {
- "$ref": "#/definitions/DataTypeDefXsd"
- },
- "value": {
- "type": "string"
- },
- "valueId": {
- "$ref": "#/definitions/Reference"
- }
- },
- "required": [
- "type",
- "valueType"
- ]
- }
- ]
- },
- "QualifierKind": {
- "type": "string",
- "enum": [
- "ConceptQualifier",
- "TemplateQualifier",
- "ValueQualifier"
- ]
- },
- "Range": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "valueType": {
- "$ref": "#/definitions/DataTypeDefXsd"
- },
- "min": {
- "type": "string"
- },
- "max": {
- "type": "string"
- },
- "modelType": {
- "const": "Range"
- }
- },
- "required": [
- "valueType"
- ]
- }
- ]
- },
- "Referable": {
- "allOf": [
- {
- "$ref": "#/definitions/HasExtensions"
- },
- {
- "properties": {
- "category": {
- "type": "string",
- "minLength": 1,
- "maxLength": 128,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "idShort": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 128
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^[a-zA-Z][a-zA-Z0-9_]*$"
- }
- ]
- },
- "displayName": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringNameType"
- },
- "minItems": 1
- },
- "description": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/LangStringTextType"
- },
- "minItems": 1
- },
- "modelType": {
- "$ref": "#/definitions/ModelType"
- }
- },
- "required": [
- "modelType"
- ]
- }
- ]
- },
- "Reference": {
- "type": "object",
- "properties": {
- "type": {
- "$ref": "#/definitions/ReferenceTypes"
- },
- "referredSemanticId": {
- "$ref": "#/definitions/Reference"
- },
- "keys": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/Key"
- },
- "minItems": 1
- }
- },
- "required": [
- "type",
- "keys"
- ]
- },
- "ReferenceElement": {
- "allOf": [
- {
- "$ref": "#/definitions/DataElement"
- },
- {
- "properties": {
- "value": {
- "$ref": "#/definitions/Reference"
- },
- "modelType": {
- "const": "ReferenceElement"
- }
- }
- }
- ]
- },
- "ReferenceTypes": {
- "type": "string",
- "enum": [
- "ExternalReference",
- "ModelReference"
- ]
- },
- "RelationshipElement": {
- "allOf": [
- {
- "$ref": "#/definitions/RelationshipElement_abstract"
- },
- {
- "properties": {
- "modelType": {
- "const": "RelationshipElement"
- }
- }
- }
- ]
- },
- "RelationshipElement_abstract": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "first": {
- "$ref": "#/definitions/Reference"
- },
- "second": {
- "$ref": "#/definitions/Reference"
- }
- },
- "required": [
- "first",
- "second"
- ]
- }
- ]
- },
- "RelationshipElement_choice": {
- "oneOf": [
- {
- "$ref": "#/definitions/RelationshipElement"
- },
- {
- "$ref": "#/definitions/AnnotatedRelationshipElement"
- }
- ]
- },
- "Resource": {
- "type": "object",
- "properties": {
- "path": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 2000
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^file:(//((localhost|(\\[((([0-9A-Fa-f]{1,4}:){6}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|::([0-9A-Fa-f]{1,4}:){5}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|([0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){4}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){3}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){2}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:){2}([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){4}[0-9A-Fa-f]{1,4})?::([0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))|(([0-9A-Fa-f]{1,4}:){5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(([0-9A-Fa-f]{1,4}:){6}[0-9A-Fa-f]{1,4})?::)|[vV][0-9A-Fa-f]+\\.([a-zA-Z0-9\\-._~]|[!$&'()*+,;=]|:)+)\\]|([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=])*)))?/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?|/((([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))+(/(([a-zA-Z0-9\\-._~]|%[0-9A-Fa-f][0-9A-Fa-f]|[!$&'()*+,;=]|[:@]))*)*)?)$"
- }
- ]
- },
- "contentType": {
- "type": "string",
- "allOf": [
- {
- "minLength": 1,
- "maxLength": 100
- },
- {
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- {
- "pattern": "^([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+/([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+([ \\t]*;[ \\t]*([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+=(([!#$%&'*+\\-.^_`|~0-9a-zA-Z])+|\"(([\\t !#-\\[\\]-~]|[\u0080-\u00ff])|\\\\([\\t !-~]|[\u0080-\u00ff]))*\"))*$"
- }
- ]
- }
- },
- "required": [
- "path"
- ]
- },
- "SpecificAssetId": {
- "allOf": [
- {
- "$ref": "#/definitions/HasSemantics"
- },
- {
- "properties": {
- "name": {
- "type": "string",
- "minLength": 1,
- "maxLength": 64,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "externalSubjectId": {
- "$ref": "#/definitions/Reference"
- }
- },
- "required": [
- "name",
- "value"
- ]
- }
- ]
- },
- "StateOfEvent": {
- "type": "string",
- "enum": [
- "off",
- "on"
- ]
- },
- "Submodel": {
- "allOf": [
- {
- "$ref": "#/definitions/Identifiable"
- },
- {
- "$ref": "#/definitions/HasKind"
- },
- {
- "$ref": "#/definitions/HasSemantics"
- },
- {
- "$ref": "#/definitions/Qualifiable"
- },
- {
- "$ref": "#/definitions/HasDataSpecification"
- },
- {
- "properties": {
- "submodelElements": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SubmodelElement_choice"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "Submodel"
- }
- }
- }
- ]
- },
- "SubmodelElement": {
- "allOf": [
- {
- "$ref": "#/definitions/Referable"
- },
- {
- "$ref": "#/definitions/HasSemantics"
- },
- {
- "$ref": "#/definitions/Qualifiable"
- },
- {
- "$ref": "#/definitions/HasDataSpecification"
- }
- ]
- },
- "SubmodelElementCollection": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "value": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SubmodelElement_choice"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "SubmodelElementCollection"
- }
- }
- }
- ]
- },
- "SubmodelElementList": {
- "allOf": [
- {
- "$ref": "#/definitions/SubmodelElement"
- },
- {
- "properties": {
- "orderRelevant": {
- "type": "boolean"
- },
- "semanticIdListElement": {
- "$ref": "#/definitions/Reference"
- },
- "typeValueListElement": {
- "$ref": "#/definitions/AasSubmodelElements"
- },
- "valueTypeListElement": {
- "$ref": "#/definitions/DataTypeDefXsd"
- },
- "value": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/SubmodelElement_choice"
- },
- "minItems": 1
- },
- "modelType": {
- "const": "SubmodelElementList"
- }
- },
- "required": [
- "typeValueListElement"
- ]
- }
- ]
- },
- "SubmodelElement_choice": {
- "oneOf": [
- {
- "$ref": "#/definitions/RelationshipElement"
- },
- {
- "$ref": "#/definitions/AnnotatedRelationshipElement"
- },
- {
- "$ref": "#/definitions/BasicEventElement"
- },
- {
- "$ref": "#/definitions/Blob"
- },
- {
- "$ref": "#/definitions/Capability"
- },
- {
- "$ref": "#/definitions/Entity"
- },
- {
- "$ref": "#/definitions/File"
- },
- {
- "$ref": "#/definitions/MultiLanguageProperty"
- },
- {
- "$ref": "#/definitions/Operation"
- },
- {
- "$ref": "#/definitions/Property"
- },
- {
- "$ref": "#/definitions/Range"
- },
- {
- "$ref": "#/definitions/ReferenceElement"
- },
- {
- "$ref": "#/definitions/SubmodelElementCollection"
- },
- {
- "$ref": "#/definitions/SubmodelElementList"
- }
- ]
- },
- "ValueList": {
- "type": "object",
- "properties": {
- "valueReferencePairs": {
- "type": "array",
- "items": {
- "$ref": "#/definitions/ValueReferencePair"
- },
- "minItems": 1
- }
- },
- "required": [
- "valueReferencePairs"
- ]
- },
- "ValueReferencePair": {
- "type": "object",
- "properties": {
- "value": {
- "type": "string",
- "minLength": 1,
- "maxLength": 2000,
- "pattern": "^([\\t\\n\\r -\ud7ff\ue000-\ufffd]|\\ud800[\\udc00-\\udfff]|[\\ud801-\\udbfe][\\udc00-\\udfff]|\\udbff[\\udc00-\\udfff])*$"
- },
- "valueId": {
- "$ref": "#/definitions/Reference"
- }
- },
- "required": [
- "value",
- "valueId"
- ]
- }
- }
-}
\ No newline at end of file
diff --git a/compliance_tool/aas_compliance_tool/schemas/aasXMLSchema.xsd b/compliance_tool/aas_compliance_tool/schemas/aasXMLSchema.xsd
deleted file mode 100644
index 95096ecb..00000000
--- a/compliance_tool/aas_compliance_tool/schemas/aasXMLSchema.xsd
+++ /dev/null
@@ -1,1344 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/compliance_tool/test/test_compliance_check_json.py b/compliance_tool/test/test_compliance_check_json.py
index a63d3909..656d1e50 100644
--- a/compliance_tool/test/test_compliance_check_json.py
+++ b/compliance_tool/test/test_compliance_check_json.py
@@ -12,50 +12,6 @@
class ComplianceToolJsonTest(unittest.TestCase):
- def test_check_schema(self) -> None:
- manager = ComplianceToolStateManager()
- script_dir = os.path.dirname(__file__)
- file_path_1 = os.path.join(script_dir, 'files/test_not_found.json')
- compliance_tool.check_schema(file_path_1, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.FAILED, manager.steps[0].status)
- self.assertEqual(Status.NOT_EXECUTED, manager.steps[1].status)
- self.assertEqual(Status.NOT_EXECUTED, manager.steps[2].status)
- self.assertIn("No such file or directory", manager.format_step(0, verbose_level=1))
-
- manager.steps = []
- file_path_2 = os.path.join(script_dir, 'files/test_not_deserializable.json')
- compliance_tool.check_schema(file_path_2, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.FAILED, manager.steps[1].status)
- self.assertEqual(Status.NOT_EXECUTED, manager.steps[2].status)
- self.assertIn("Expecting ',' delimiter: line 4 column 2 (char 54)", manager.format_step(1, verbose_level=1))
-
- manager.steps = []
- file_path_3 = os.path.join(script_dir, 'files/test_empty.json')
- compliance_tool.check_schema(file_path_3, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
- manager.steps = []
- file_path_4 = os.path.join(script_dir, 'files/test_demo_full_example.json')
- compliance_tool.check_schema(file_path_4, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
- manager.steps = []
- file_path_5 = os.path.join(script_dir, 'files/test_demo_full_example_wrong_attribute.json')
- compliance_tool.check_schema(file_path_5, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
def test_check_deserialization(self) -> None:
manager = ComplianceToolStateManager()
script_dir = os.path.dirname(__file__)
diff --git a/compliance_tool/test/test_compliance_check_xml.py b/compliance_tool/test/test_compliance_check_xml.py
index c7b023cc..7f5fbecc 100644
--- a/compliance_tool/test/test_compliance_check_xml.py
+++ b/compliance_tool/test/test_compliance_check_xml.py
@@ -12,41 +12,6 @@
class ComplianceToolXmlTest(unittest.TestCase):
- def test_check_schema(self) -> None:
- manager = ComplianceToolStateManager()
- script_dir = os.path.dirname(__file__)
- file_path_1 = os.path.join(script_dir, 'files/test_not_found.xml')
- compliance_tool.check_schema(file_path_1, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.FAILED, manager.steps[0].status)
- self.assertEqual(Status.NOT_EXECUTED, manager.steps[1].status)
- self.assertEqual(Status.NOT_EXECUTED, manager.steps[2].status)
- self.assertIn("No such file or directory", manager.format_step(0, verbose_level=1))
-
- manager.steps = []
- file_path_2 = os.path.join(script_dir, 'files/test_empty.xml')
- compliance_tool.check_schema(file_path_2, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
- manager.steps = []
- file_path_3 = os.path.join(script_dir, 'files/test_demo_full_example.xml')
- compliance_tool.check_schema(file_path_3, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
- manager.steps = []
- file_path_4 = os.path.join(script_dir, 'files/test_demo_full_example_wrong_attribute.xml')
- compliance_tool.check_schema(file_path_4, manager)
- self.assertEqual(3, len(manager.steps))
- self.assertEqual(Status.SUCCESS, manager.steps[0].status)
- self.assertEqual(Status.SUCCESS, manager.steps[1].status)
- self.assertEqual(Status.SUCCESS, manager.steps[2].status)
-
def test_check_deserialization(self) -> None:
manager = ComplianceToolStateManager()
script_dir = os.path.dirname(__file__)
diff --git a/sdk/basyx/aas/adapter/aasx.py b/sdk/basyx/aas/adapter/aasx.py
index 2c7fe0b4..82fe4b76 100644
--- a/sdk/basyx/aas/adapter/aasx.py
+++ b/sdk/basyx/aas/adapter/aasx.py
@@ -880,6 +880,7 @@ def rename_file(self, old_name: str, new_name: str) -> str:
if new_name == old_name:
return new_name
file_hash, file_content_type = self._name_map[old_name]
+ self._store_refcount[file_hash] -= 1
del self._name_map[old_name]
return self._assign_unique_name(new_name, file_hash, file_content_type)
@@ -889,6 +890,7 @@ def _assign_unique_name(self, name: str, sha: bytes, content_type: str) -> str:
while True:
if new_name not in self._name_map:
self._name_map[new_name] = (sha, content_type)
+ self._store_refcount[sha] += 1
return new_name
elif self._name_map[new_name] == (sha, content_type):
return new_name
diff --git a/sdk/basyx/aas/adapter/xml/xml_deserialization.py b/sdk/basyx/aas/adapter/xml/xml_deserialization.py
index b36dddb9..2330b9af 100644
--- a/sdk/basyx/aas/adapter/xml/xml_deserialization.py
+++ b/sdk/basyx/aas/adapter/xml/xml_deserialization.py
@@ -1158,7 +1158,7 @@ def construct_data_specification_iec61360(cls, element: etree._Element,
if value_list is not None:
ds_iec.value_list = value_list
value = _get_text_or_none(element.find(NS_AAS + "value"))
- if value is not None and value_format is not None:
+ if value is not None:
ds_iec.value = value
level_type = element.find(NS_AAS + "levelType")
if level_type is not None:
diff --git a/sdk/basyx/aas/backend/couchdb.py b/sdk/basyx/aas/backend/couchdb.py
index c2871aed..fe02345e 100644
--- a/sdk/basyx/aas/backend/couchdb.py
+++ b/sdk/basyx/aas/backend/couchdb.py
@@ -160,7 +160,6 @@ def get_identifiable_by_couchdb_id(self, couchdb_id: str) -> model.Identifiable:
raise KeyError("No Identifiable with couchdb-id {} found in CouchDB database".format(couchdb_id)) from e
raise
- # Add CouchDB metadata (for later commits) to object
obj = data['data']
if not isinstance(obj, model.Identifiable):
raise CouchDBResponseError("The CouchDB document with id {} does not contain an identifiable AAS object."
@@ -168,14 +167,10 @@ def get_identifiable_by_couchdb_id(self, couchdb_id: str) -> model.Identifiable:
set_couchdb_revision("{}/{}/{}".format(self.url, self.database_name, urllib.parse.quote(couchdb_id, safe='')),
data["_rev"])
- # If we still have a local replication of that object (since it is referenced from anywhere else), update that
- # replication and return it.
with self._object_cache_lock:
if obj.id in self._object_cache:
- old_obj = self._object_cache[obj.id]
- old_obj.update_from(obj)
- return old_obj
- self._object_cache[obj.id] = obj
+ return self._object_cache[obj.id]
+ self._object_cache[obj.id] = obj
return obj
def get_item(self, identifier: model.Identifier) -> model.Identifiable:
@@ -186,6 +181,9 @@ def get_item(self, identifier: model.Identifier) -> model.Identifiable:
:raises CouchDBError: If error occur during the request to the CouchDB server
(see ``_do_request()`` for details)
"""
+ with self._object_cache_lock:
+ if identifier in self._object_cache:
+ return self._object_cache[identifier]
try:
return self.get_identifiable_by_couchdb_id(self._transform_id(identifier, False))
except KeyError as e:
@@ -220,6 +218,37 @@ def add(self, x: model.Identifiable) -> None:
with self._object_cache_lock:
self._object_cache[x.id] = x
+ def commit(self, x: model.Identifiable) -> None:
+ """
+ Write the current in-memory state of a stored object back to the CouchDB.
+
+ :param x: The object to persist
+ :raises KeyError: If the object is not present in the store or no revision is known
+ :raises CouchDBConflictError: If the object was modified in the database since it was last fetched
+ :raises CouchDBError: If error occur during the request to the CouchDB server
+ (see ``_do_request()`` for details)
+ """
+ doc_url = "{}/{}/{}".format(self.url, self.database_name, self._transform_id(x.id))
+ rev = get_couchdb_revision(doc_url)
+ if rev is None:
+ raise KeyError("No revision found for object with id {} — not fetched from this store".format(x.id))
+ data = json.dumps({'data': x}, cls=json_serialization.AASToJsonEncoder)
+ try:
+ response = self._do_request(
+ "{}?rev={}".format(doc_url, rev),
+ 'PUT',
+ {'Content-type': 'application/json'},
+ data.encode('utf-8'))
+ set_couchdb_revision(doc_url, response["rev"])
+ except CouchDBServerError as e:
+ if e.code == 404:
+ raise KeyError("No AAS object with id {} exists in CouchDB database".format(x.id)) from e
+ elif e.code == 409:
+ raise CouchDBConflictError(
+ "Object with id {} has been modified in the database since it was last fetched."
+ .format(x.id)) from e
+ raise
+
def discard(self, x: model.Identifiable, safe_delete=False) -> None:
"""
Delete an :class:`~basyx.aas.model.base.Identifiable` AAS object from the CouchDB database
diff --git a/sdk/basyx/aas/backend/local_file.py b/sdk/basyx/aas/backend/local_file.py
index 4008497a..df21ddfe 100644
--- a/sdk/basyx/aas/backend/local_file.py
+++ b/sdk/basyx/aas/backend/local_file.py
@@ -16,6 +16,7 @@
import json
import os
import hashlib
+import tempfile
import threading
import warnings
import weakref
@@ -31,6 +32,13 @@ class LocalFileIdentifiableStore(model.AbstractObjectStore[model.Identifier, mod
"""
An ObjectStore implementation for :class:`~basyx.aas.model.base.Identifiable` BaSyx Python SDK objects backed
by a local file based local backend
+
+ .. warning::
+ This backend is intended for development and testing only. It provides no
+ concurrency control across processes: concurrent writes to the same object
+ (e.g. under a multi-worker WSGI server) will silently overwrite each other,
+ with the last writer winning and no error raised. Use a dedicated database
+ backend for any production deployment.
"""
def __init__(self, directory_path: str):
"""
@@ -68,21 +76,16 @@ def get_identifiable_by_hash(self, hash_: str) -> model.Identifiable:
:raises KeyError: If the respective file could not be found
"""
- # Try to get the correct file
try:
with open("{}/{}.json".format(self.directory_path, hash_), "r") as file:
data = json.load(file, cls=json_deserialization.AASFromJsonDecoder)
obj = data["data"]
except FileNotFoundError as e:
raise KeyError("No Identifiable with hash {} found in local file database".format(hash_)) from e
- # If we still have a local replication of that object (since it is referenced from anywhere else), update that
- # replication and return it.
with self._object_cache_lock:
if obj.id in self._object_cache:
- old_obj = self._object_cache[obj.id]
- old_obj.update_from(obj)
- return old_obj
- self._object_cache[obj.id] = obj
+ return self._object_cache[obj.id]
+ self._object_cache[obj.id] = obj
return obj
def get_item(self, identifier: model.Identifier) -> model.Identifiable:
@@ -91,11 +94,33 @@ def get_item(self, identifier: model.Identifier) -> model.Identifiable:
:raises KeyError: If the respective file could not be found
"""
+ with self._object_cache_lock:
+ if identifier in self._object_cache:
+ return self._object_cache[identifier]
try:
return self.get_identifiable_by_hash(self._transform_id(identifier))
except KeyError as e:
raise KeyError("No Identifiable with id {} found in local file database".format(identifier)) from e
+ def _write_atomic(self, x: model.Identifiable) -> None:
+ """
+ Serialize x to a temp file in the store directory, then atomically replace the final file.
+
+ Using os.replace() (rename(2) on POSIX) ensures readers always see a complete file — never
+ a partially-written one from a crash or concurrent access mid-write.
+ """
+ final_path = "{}/{}.json".format(self.directory_path, self._transform_id(x.id))
+ tmp_fd, tmp_path = tempfile.mkstemp(dir=self.directory_path, suffix=".tmp")
+ try:
+ with os.fdopen(tmp_fd, "w") as tmp_file:
+ json.dump({"data": x}, tmp_file, cls=json_serialization.AASToJsonEncoder, indent=4)
+ os.replace(tmp_path, final_path)
+ # Catch all `Exception`s, as well as `KeyboardInterrupt` and `SystemExit` too, so the temp
+ # file is never left behind even if the process is being torn down:
+ except BaseException:
+ os.unlink(tmp_path)
+ raise
+
def add(self, x: model.Identifiable) -> None:
"""
Add an object to the store
@@ -105,10 +130,20 @@ def add(self, x: model.Identifiable) -> None:
logger.debug("Adding object %s to Local File Store ...", repr(x))
if os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
raise KeyError("Identifiable with id {} already exists in local file database".format(x.id))
- with open("{}/{}.json".format(self.directory_path, self._transform_id(x.id)), "w") as file:
- json.dump({"data": x}, file, cls=json_serialization.AASToJsonEncoder, indent=4)
- with self._object_cache_lock:
- self._object_cache[x.id] = x
+ self._write_atomic(x)
+ with self._object_cache_lock:
+ self._object_cache[x.id] = x
+
+ def commit(self, x: model.Identifiable) -> None:
+ """
+ Write the current in-memory state of a stored object back to its file.
+
+ :param x: The object to persist
+ :raises KeyError: If the object is not present in the store
+ """
+ if not os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
+ raise KeyError("No AAS object with id {} exists in local file database".format(x.id))
+ self._write_atomic(x)
def discard(self, x: model.Identifiable) -> None:
"""
@@ -150,7 +185,7 @@ def __len__(self) -> int:
:return: The number of objects (determined from the number of documents)
"""
logger.debug("Fetching number of documents from database ...")
- return len(os.listdir(self.directory_path))
+ return sum(1 for f in os.listdir(self.directory_path) if f.lower().endswith(".json"))
def __iter__(self) -> Iterator[model.Identifiable]:
"""
@@ -161,7 +196,8 @@ def __iter__(self) -> Iterator[model.Identifiable]:
"""
logger.debug("Iterating over objects in database ...")
for name in os.listdir(self.directory_path):
- yield self.get_identifiable_by_hash(name.rstrip(".json"))
+ if name.lower().endswith(".json"):
+ yield self.get_identifiable_by_hash(name[:-5])
@staticmethod
def _transform_id(identifier: model.Identifier) -> str:
diff --git a/sdk/basyx/aas/model/base.py b/sdk/basyx/aas/model/base.py
index 6c6eb25e..718c0d63 100644
--- a/sdk/basyx/aas/model/base.py
+++ b/sdk/basyx/aas/model/base.py
@@ -2077,8 +2077,10 @@ def discard(self, x: _NSO) -> None:
def pop(self) -> _NSO:
_, value = next(iter(self._backend.values()))[0].popitem()
+ for key_attr_name, (backend_dict, case_sensitive) in self._backend.items():
+ key_attr_value = self._get_attribute(value, key_attr_name, case_sensitive)
+ backend_dict.pop(key_attr_value, None)
self._execute_item_del_hook(value)
- value.parent = None
return value
def clear(self) -> None:
diff --git a/sdk/basyx/aas/model/provider.py b/sdk/basyx/aas/model/provider.py
index c48342c6..9a91a346 100644
--- a/sdk/basyx/aas/model/provider.py
+++ b/sdk/basyx/aas/model/provider.py
@@ -59,6 +59,18 @@ class AbstractObjectStore(AbstractObjectProvider[_KEY, _VALUE], MutableSet[_VALU
def __init__(self):
pass
+ def commit(self, x: _VALUE) -> None:
+ """
+ Persist an in-memory mutation of a stored object back to the underlying storage.
+
+ Persistent backends (e.g. file-based or database-backed stores) must override this to
+ write the updated object back to storage. In-memory stores should override this with an
+ explicit no-op to make the intent clear.
+
+ :param x: The object whose current in-memory state should be persisted
+ """
+ raise NotImplementedError()
+
def update(self, other: Iterable[_VALUE]) -> None:
for x in other:
self.add(x)
@@ -146,6 +158,9 @@ def add(self, x: _IDENTIFIABLE) -> None:
.format(x.id))
self._backend[x.id] = x
+ def commit(self, x: _IDENTIFIABLE) -> None:
+ pass
+
def discard(self, x: _IDENTIFIABLE) -> None:
if self._backend.get(x.id) is x:
del self._backend[x.id]
@@ -223,6 +238,9 @@ def add(self, x: _IDENTIFIABLE) -> None:
else:
raise KeyError(f"Identifiable object with same id {x.id} is already stored in this store")
+ def commit(self, x: _IDENTIFIABLE) -> None:
+ pass
+
def discard(self, x: _IDENTIFIABLE) -> None:
self._backend.discard(x)
diff --git a/sdk/test/adapter/aasx/test_aasx.py b/sdk/test/adapter/aasx/test_aasx.py
index 271b992c..e4ab1a1d 100644
--- a/sdk/test/adapter/aasx/test_aasx.py
+++ b/sdk/test/adapter/aasx/test_aasx.py
@@ -89,6 +89,24 @@ def test_supplementary_file_container(self) -> None:
with self.assertRaises(KeyError):
container.write_file(duplicate_file, file_content)
+ def test_supplementary_file_container_refcount(self) -> None:
+ container = aasx.DictSupplementaryFileContainer()
+ data = b"test content"
+ name1 = container.add_file("/file1.bin", io.BytesIO(data), "application/octet-stream")
+ name2 = container.add_file("/file2.bin", io.BytesIO(data), "application/octet-stream")
+ content_hash = container.get_sha256(name1)
+
+ # Both names point to same content — backing store must be present
+ self.assertIn(content_hash, container._store)
+
+ # Deleting one reference must NOT free the backing store
+ container.delete_file(name1)
+ self.assertIn(content_hash, container._store)
+
+ # Deleting the last reference must free the backing store
+ container.delete_file(name2)
+ self.assertNotIn(content_hash, container._store)
+
class AASXWriterTest(unittest.TestCase):
def test_write_missing_aas_objects(self):
diff --git a/sdk/test/adapter/xml/test_xml_deserialization.py b/sdk/test/adapter/xml/test_xml_deserialization.py
index 2857a9dc..14a5041b 100644
--- a/sdk/test/adapter/xml/test_xml_deserialization.py
+++ b/sdk/test/adapter/xml/test_xml_deserialization.py
@@ -428,6 +428,50 @@ def test_stripped_asset_administration_shell(self) -> None:
self.assertEqual(len(aas.submodel), 0)
+class XmlDeserializationDataSpecTest(unittest.TestCase):
+ def test_data_spec_iec61360_value_without_value_format(self) -> None:
+ xml = _xml_wrap(f"""
+
+
+ http://example.org/test_cd
+
+
+
+ ExternalReference
+
+
+ GlobalReference
+ https://admin-shell.io/DataSpecificationTemplates/DataSpecificationIec61360/3/0
+
+
+
+
+
+
+
+ en
+ Test
+
+
+ test_value
+
+
+
+
+
+
+ """)
+ object_store = read_aas_xml_file(io.StringIO(xml), failsafe=False)
+ cd = object_store.get_item("http://example.org/test_cd")
+ self.assertIsInstance(cd, model.ConceptDescription)
+ assert isinstance(cd, model.ConceptDescription)
+ ds_content = list(cd.embedded_data_specifications)[0].data_specification_content
+ self.assertIsInstance(ds_content, model.DataSpecificationIEC61360)
+ assert isinstance(ds_content, model.DataSpecificationIEC61360)
+ self.assertEqual("test_value", ds_content.value)
+ self.assertIsNone(ds_content.value_format)
+
+
class XmlDeserializationDerivingTest(unittest.TestCase):
def test_submodel_constructor_overriding(self) -> None:
class EnhancedSubmodel(model.Submodel):
diff --git a/sdk/test/backend/test_local_file.py b/sdk/test/backend/test_local_file.py
index adcbfcc7..71447f61 100644
--- a/sdk/test/backend/test_local_file.py
+++ b/sdk/test/backend/test_local_file.py
@@ -4,6 +4,7 @@
# the LICENSE file of this project.
#
# SPDX-License-Identifier: MIT
+import gc
import os.path
import shutil
@@ -107,6 +108,59 @@ def test_key_errors(self) -> None:
self.assertEqual("'No AAS object with id https://example.org/Test_Submodel exists in "
"local file database'", str(cm.exception))
+ def test_add_and_len_consistent(self) -> None:
+ # Each add() must increment len() by exactly 1
+ example_data = list(create_full_example())
+ for i, item in enumerate(example_data):
+ self.identifiable_store.add(item)
+ self.assertEqual(i + 1, len(self.identifiable_store))
+
+ # Stray non-json file must not be counted
+ stray = os.path.join(store_path, ".DS_Store")
+ with open(stray, "w") as f:
+ f.write("stray")
+ self.assertEqual(len(example_data), len(self.identifiable_store))
+ os.remove(stray)
+
+ def test_iter_ignores_non_json_files(self) -> None:
+ example_data = create_full_example()
+ for item in example_data:
+ self.identifiable_store.add(item)
+
+ # Stray files must not crash the iterator or be yielded
+ stray = os.path.join(store_path, ".DS_Store")
+ with open(stray, "w") as f:
+ f.write("stray")
+ items = list(self.identifiable_store)
+ self.assertEqual(5, len(items))
+ os.remove(stray)
+
+ def test_mutation_persistence(self) -> None:
+ submodel = model.Submodel(
+ id_='https://example.org/MutationTest',
+ submodel_element={
+ model.Property(id_short='Prop', value_type=model.datatypes.String, value='before')
+ }
+ )
+ self.identifiable_store.add(submodel)
+
+ retrieved = self.identifiable_store.get_item('https://example.org/MutationTest')
+ assert isinstance(retrieved, model.Submodel)
+ prop = retrieved.get_referable(['Prop'])
+ assert isinstance(prop, model.Property)
+ prop.update_from(model.Property(id_short='Prop', value_type=model.datatypes.String, value='after'))
+ self.identifiable_store.commit(retrieved)
+
+ # Drop all strong references to evict the WeakValueDictionary cache
+ del submodel, retrieved, prop
+ gc.collect()
+
+ fresh = self.identifiable_store.get_item('https://example.org/MutationTest')
+ assert isinstance(fresh, model.Submodel)
+ fresh_prop = fresh.get_referable(['Prop'])
+ assert isinstance(fresh_prop, model.Property)
+ self.assertEqual('after', fresh_prop.value)
+
def test_reload_discard(self) -> None:
# Load example submodel
example_submodel = create_example_submodel()
diff --git a/sdk/test/model/test_base.py b/sdk/test/model/test_base.py
index c5b0429d..3a74a774 100644
--- a/sdk/test/model/test_base.py
+++ b/sdk/test/model/test_base.py
@@ -338,6 +338,18 @@ def setUp(self):
self.namespace = self._namespace_class()
self.namespace3 = self._namespace_class_qualifier()
+ def test_namespaceset_pop_removes_from_all_backends(self) -> None:
+ # set1 has two backends: id_short and semantic_id
+ self.namespace.set1.add(self.prop1)
+ popped = self.namespace.set1.pop()
+ self.assertIs(self.prop1, popped)
+ self.assertEqual(0, len(self.namespace.set1))
+ # After pop, adding a new item with the same semantic_id must NOT raise AASConstraintViolation —
+ # it would if the popped item's semantic_id entry were still in the backend
+ new_prop = model.Property("NewProp", model.datatypes.Int, semantic_id=self.propSemanticID)
+ self.namespace.set1.add(new_prop)
+ self.assertEqual(1, len(self.namespace.set1))
+
def test_NamespaceSet(self) -> None:
self.namespace.set1.add(self.prop1)
self.assertEqual(1, len(self.namespace.set1))
diff --git a/sdk/test/model/test_provider.py b/sdk/test/model/test_provider.py
index 10947c16..2cefda23 100644
--- a/sdk/test/model/test_provider.py
+++ b/sdk/test/model/test_provider.py
@@ -55,6 +55,24 @@ def test_store_update(self) -> None:
self.assertIsInstance(identifiable_store1, model.DictIdentifiableStore)
self.assertIn(self.aas2, identifiable_store1)
+ def test_store_sync(self) -> None:
+ aas_identifiable_store: model.DictIdentifiableStore[model.Identifiable] = model.DictIdentifiableStore()
+
+ self.assertEqual(aas_identifiable_store.sync([self.aas1, self.aas2], overwrite=False), (2, 0, 0))
+ self.assertIn(self.aas1, aas_identifiable_store)
+ self.assertIn(self.aas2, aas_identifiable_store)
+
+ self.assertEqual(aas_identifiable_store.sync([self.aas1], overwrite=False), (0, 0, 1))
+
+ self.assertEqual(aas_identifiable_store.sync([self.aas1], overwrite=True), (0, 1, 0))
+ self.assertIn(self.aas1, aas_identifiable_store)
+
+ self.assertEqual(aas_identifiable_store.sync([self.aas1, self.submodel1], overwrite=True), (1, 1, 0))
+
+ self.assertEqual(aas_identifiable_store.sync([self.aas1, self.submodel2], overwrite=False), (1, 0, 1))
+
+ self.assertEqual(aas_identifiable_store.sync([], overwrite=False), (0, 0, 0))
+
def test_provider_multiplexer(self) -> None:
aas_identifiable_store: model.DictIdentifiableStore[model.Identifiable] = (
model.DictIdentifiableStore()
diff --git a/server/app/adapter/jsonization.py b/server/app/adapter/jsonization.py
index a8ee3471..897590c7 100644
--- a/server/app/adapter/jsonization.py
+++ b/server/app/adapter/jsonization.py
@@ -1,10 +1,10 @@
import logging
-from typing import Callable, Dict, Optional, Set, Type
+from typing import Callable, Dict, Type
from basyx.aas import model
-from basyx.aas.adapter._generic import ASSET_KIND, ASSET_KIND_INVERSE, JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES, PathOrIO
+from basyx.aas.adapter._generic import ASSET_KIND, ASSET_KIND_INVERSE, JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES
from basyx.aas.adapter.json import AASToJsonEncoder
-from basyx.aas.adapter.json.json_deserialization import AASFromJsonDecoder, _get_ts, read_aas_json_file_into
+from basyx.aas.adapter.json.json_deserialization import AASFromJsonDecoder, _get_ts
import app.model as server_model
@@ -207,27 +207,6 @@ class ServerStrictStrippedAASFromJsonDecoder(ServerStrictAASFromJsonDecoder, Ser
pass
-def read_server_aas_json_file_into(
- object_store: model.AbstractObjectStore,
- file: PathOrIO,
- replace_existing: bool = False,
- ignore_existing: bool = False,
- failsafe: bool = True,
- stripped: bool = False,
- decoder: Optional[Type[AASFromJsonDecoder]] = None,
-) -> Set[model.Identifier]:
- return read_aas_json_file_into(
- object_store=object_store,
- file=file,
- replace_existing=replace_existing,
- ignore_existing=ignore_existing,
- failsafe=failsafe,
- stripped=stripped,
- decoder=decoder,
- keys_to_types=JSON_SERVER_AAS_TOP_LEVEL_KEYS_TO_TYPES,
- )
-
-
class ServerAASToJsonEncoder(AASToJsonEncoder):
@classmethod
diff --git a/server/app/backend/local_file.py b/server/app/backend/local_file.py
index e55c08e6..e71a0e98 100644
--- a/server/app/backend/local_file.py
+++ b/server/app/backend/local_file.py
@@ -67,20 +67,15 @@ def get_descriptor_by_hash(self, hash_: str) -> _DESCRIPTOR_TYPE:
:raises KeyError: If the respective file could not be found
"""
- # Try to get the correct file
try:
with open("{}/{}.json".format(self.directory_path, hash_), "r") as file:
obj = json.load(file, cls=jsonization.ServerAASFromJsonDecoder)
except FileNotFoundError as e:
raise KeyError("No Descriptor with hash {} found in local file database".format(hash_)) from e
- # If we still have a local replication of that object (since it is referenced from anywhere else), update that
- # replication and return it.
with self._object_cache_lock:
if obj.id in self._object_cache:
- old_obj = self._object_cache[obj.id]
- old_obj.update_from(obj)
- return old_obj
- self._object_cache[obj.id] = obj
+ return self._object_cache[obj.id]
+ self._object_cache[obj.id] = obj
return obj
def get_item(self, identifier: model.Identifier) -> _DESCRIPTOR_TYPE:
@@ -89,6 +84,9 @@ def get_item(self, identifier: model.Identifier) -> _DESCRIPTOR_TYPE:
:raises KeyError: If the respective file could not be found
"""
+ with self._object_cache_lock:
+ if identifier in self._object_cache:
+ return self._object_cache[identifier]
try:
return self.get_descriptor_by_hash(self._transform_id(identifier))
except KeyError as e:
@@ -113,6 +111,20 @@ def add(self, x: _DESCRIPTOR_TYPE) -> None:
with self._object_cache_lock:
self._object_cache[x.id] = x
+ def commit(self, x: _DESCRIPTOR_TYPE) -> None:
+ """
+ Write the current in-memory state of a stored descriptor back to its file.
+
+ :param x: The descriptor to persist
+ :raises KeyError: If the descriptor is not present in the store
+ """
+ if not os.path.exists("{}/{}.json".format(self.directory_path, self._transform_id(x.id))):
+ raise KeyError("No AAS Descriptor object with id {} exists in local file database".format(x.id))
+ with open("{}/{}.json".format(self.directory_path, self._transform_id(x.id)), "w") as file:
+ serialized = json.loads(json.dumps(x, cls=jsonization.ServerAASToJsonEncoder))
+ serialized["modelType"] = DESCRIPTOR_TYPE_TO_STRING[type(x)]
+ json.dump(serialized, file, indent=4)
+
def discard(self, x: _DESCRIPTOR_TYPE) -> None:
"""
Delete an :class:`~app.model.descriptor.Descriptor` AAS object from the local file store
diff --git a/server/app/interfaces/base.py b/server/app/interfaces/base.py
index d3231237..865b599b 100644
--- a/server/app/interfaces/base.py
+++ b/server/app/interfaces/base.py
@@ -10,7 +10,7 @@
import io
import itertools
import json
-from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, TypeVar, Union
+from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Protocol, Tuple, Type, TypeVar, Union, runtime_checkable
import werkzeug.exceptions
import werkzeug.routing
@@ -295,6 +295,24 @@ def http_exception_to_response(
return response_type(result, status=exception.code, headers=headers)
+@runtime_checkable
+class QueryableObjectStore(Protocol):
+ """Structural protocol for object stores that support AASQL querying.
+
+ Implement ``query(aasql_body, return_var)`` to advertise query support.
+ No explicit inheritance required — duck-typing via ``isinstance`` works at runtime.
+ """
+
+ def query(self, aasql_body: str, return_var: str) -> List[dict]:
+ """Execute an AASQL query and return matching serialized AAS objects.
+
+ :param aasql_body: raw AASQL JSON string
+ :param return_var: Cypher return variable name (``"sm"`` or ``"aas"``)
+ :return: list of serialized AAS/Submodel dicts
+ """
+ ...
+
+
class ObjectStoreWSGIApp(BaseWSGIApp):
object_store: AbstractObjectStore
diff --git a/server/app/interfaces/registry.py b/server/app/interfaces/registry.py
index 37ab9555..274602ff 100644
--- a/server/app/interfaces/registry.py
+++ b/server/app/interfaces/registry.py
@@ -180,7 +180,7 @@ def post_aas_descriptor(
self.object_store.add(descriptor)
except KeyError as e:
raise Conflict(f"AssetAdministrationShellDescriptor with Identifier {descriptor.id} already exists!") from e
- descriptor.commit()
+ self.object_store.commit(descriptor)
created_resource_url = map_adapter.build(
self.get_aas_descriptor_by_id, {"aas_id": descriptor.id}, force_external=True
)
@@ -202,12 +202,12 @@ def put_aas_descriptor_by_id(
request, server_model.AssetAdministrationShellDescriptor, is_stripped_request(request)
)
)
- descriptor.commit()
+ self.object_store.commit(descriptor)
return response_t()
except NotFound:
descriptor = HTTPApiDecoder.request_body(request, server_model.AssetAdministrationShellDescriptor, False)
self.object_store.add(descriptor)
- descriptor.commit()
+ self.object_store.commit(descriptor)
created_resource_url = map_adapter.build(
self.get_aas_descriptor_by_id, {"aas_id": descriptor.id}, force_external=True
)
@@ -247,7 +247,7 @@ def post_submodel_descriptor_through_superpath(
if any(sd.id == submodel_descriptor.id for sd in aas_descriptor.submodel_descriptors):
raise Conflict(f"Submodel Descriptor with Identifier {submodel_descriptor.id} already exists!")
aas_descriptor.submodel_descriptors.append(submodel_descriptor)
- aas_descriptor.commit()
+ self.object_store.commit(aas_descriptor)
created_resource_url = map_adapter.build(
self.get_submodel_descriptor_by_id_through_superpath,
{"aas_id": aas_descriptor.id, "submodel_id": submodel_descriptor.id},
@@ -269,14 +269,14 @@ def put_submodel_descriptor_by_id_through_superpath(
submodel_descriptor.update_from(
HTTPApiDecoder.request_body(request, server_model.SubmodelDescriptor, is_stripped_request(request))
)
- aas_descriptor.commit()
+ self.object_store.commit(aas_descriptor)
return response_t()
except NotFound:
submodel_descriptor = HTTPApiDecoder.request_body(
request, server_model.SubmodelDescriptor, is_stripped_request(request)
)
aas_descriptor.submodel_descriptors.append(submodel_descriptor)
- aas_descriptor.commit()
+ self.object_store.commit(aas_descriptor)
created_resource_url = map_adapter.build(
self.get_submodel_descriptor_by_id_through_superpath,
{"aas_id": aas_descriptor.id, "submodel_id": submodel_descriptor.id},
@@ -293,7 +293,7 @@ def delete_submodel_descriptor_by_id_through_superpath(
if submodel_descriptor is None:
raise NotFound(f"Submodel Descriptor with Identifier {submodel_id} not found in AssetAdministrationShell!")
aas_descriptor.submodel_descriptors.remove(submodel_descriptor)
- aas_descriptor.commit()
+ self.object_store.commit(aas_descriptor)
return response_t()
# ------ Submodel REGISTRY ROUTES -------
@@ -321,7 +321,7 @@ def post_submodel_descriptor(
self.object_store.add(submodel_descriptor)
except KeyError as e:
raise Conflict(f"Submodel Descriptor with Identifier {submodel_descriptor.id} already exists!") from e
- submodel_descriptor.commit()
+ self.object_store.commit(submodel_descriptor)
created_resource_url = map_adapter.build(
self.get_submodel_descriptor_by_id, {"submodel_id": submodel_descriptor.id}, force_external=True
)
@@ -335,14 +335,14 @@ def put_submodel_descriptor_by_id(
submodel_descriptor.update_from(
HTTPApiDecoder.request_body(request, server_model.SubmodelDescriptor, is_stripped_request(request))
)
- submodel_descriptor.commit()
+ self.object_store.commit(submodel_descriptor)
return response_t()
except NotFound:
submodel_descriptor = HTTPApiDecoder.request_body(
request, server_model.SubmodelDescriptor, is_stripped_request(request)
)
self.object_store.add(submodel_descriptor)
- submodel_descriptor.commit()
+ self.object_store.commit(submodel_descriptor)
created_resource_url = map_adapter.build(
self.get_submodel_descriptor_by_id, {"submodel_id": submodel_descriptor.id}, force_external=True
)
diff --git a/server/app/interfaces/repository.py b/server/app/interfaces/repository.py
index 0e75eedd..59622920 100644
--- a/server/app/interfaces/repository.py
+++ b/server/app/interfaces/repository.py
@@ -24,7 +24,7 @@
from app.interfaces.base import PagingMetadata
from app.util.converters import IdentifierToBase64URLConverter, IdShortPathConverter, base64url_decode
-from .base import ObjectStoreWSGIApp, APIResponse, is_stripped_request, HTTPApiDecoder, T
+from .base import ObjectStoreWSGIApp, APIResponse, is_stripped_request, HTTPApiDecoder, QueryableObjectStore, T
from app.model import ServiceSpecificationProfileEnum, ServiceDescription
SUPPORTED_PROFILES: ServiceDescription = ServiceDescription([
@@ -32,6 +32,8 @@
ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_FULL,
ServiceSpecificationProfileEnum.AAS_REPOSITORY_READ,
ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_READ,
+ # ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY,
+ # ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY,
])
@@ -49,6 +51,8 @@ def __init__(
Submount(
base_path,
[
+ Rule("/query/shells", methods=["POST"], endpoint=self.query_shells),
+ Rule("/query/submodels", methods=["POST"], endpoint=self.query_submodels),
Rule("/serialization", methods=["GET"], endpoint=self.not_implemented),
Rule("/description", methods=["GET"], endpoint=self.get_description),
Rule("/shells", methods=["GET"], endpoint=self.get_aas_all),
@@ -470,10 +474,7 @@ def _get_shells(
for specific_asset_id in specific_asset_ids
)
)
- and (
- len(global_asset_ids) <= 1
- and (not global_asset_ids or shell.asset_information.global_asset_id in global_asset_ids)
- )
+ and (not global_asset_ids or shell.asset_information.global_asset_id in global_asset_ids)
),
aas,
)
@@ -517,12 +518,42 @@ def _get_submodel_submodel_elements_id_short_path(self, url_args: Dict) -> model
def _get_concept_description(self, url_args):
return self._get_obj_ts(url_args["concept_id"], model.ConceptDescription)
+ def query_submodels(self, request: Request, url_args: Dict, **_kwargs) -> Response:
+ if not isinstance(self.object_store, QueryableObjectStore):
+ raise werkzeug.exceptions.NotImplemented("The current store does not support AASQL queries")
+ try:
+ results = self.object_store.query(request.get_data(as_text=True), "sm")
+ except (json.JSONDecodeError, ValueError) as e:
+ raise BadRequest(f"Invalid AASQL query: {e}") from e
+ return Response(
+ json.dumps({"paging_metadata": {"resultType": "Submodel"}, "result": results}),
+ content_type="application/json",
+ )
+
+ def query_shells(self, request: Request, url_args: Dict, **_kwargs) -> Response:
+ if not isinstance(self.object_store, QueryableObjectStore):
+ raise werkzeug.exceptions.NotImplemented("The current store does not support AASQL queries")
+ try:
+ results = self.object_store.query(request.get_data(as_text=True), "aas")
+ except (json.JSONDecodeError, ValueError) as e:
+ raise BadRequest(f"Invalid AASQL query: {e}") from e
+ return Response(
+ json.dumps({"paging_metadata": {"resultType": "AssetAdministrationShell"}, "result": results}),
+ content_type="application/json",
+ )
+
# ------ all not implemented ROUTES -------
def not_implemented(self, request: Request, url_args: Dict, **_kwargs) -> Response:
raise werkzeug.exceptions.NotImplemented("This route is not implemented!")
def get_description(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response:
- return response_t(SUPPORTED_PROFILES.to_dict())
+ profiles = SUPPORTED_PROFILES.to_dict()
+ if isinstance(self.object_store, QueryableObjectStore):
+ profiles["profiles"].extend([
+ ServiceSpecificationProfileEnum.AAS_REPOSITORY_QUERY.value,
+ ServiceSpecificationProfileEnum.SUBMODEL_REPOSITORY_QUERY.value,
+ ])
+ return response_t(profiles)
# ------ AAS REPO ROUTES -------
def get_aas_all(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response:
@@ -562,6 +593,7 @@ def put_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse
aas.update_from(
HTTPApiDecoder.request_body(request, model.AssetAdministrationShell, is_stripped_request(request))
)
+ self.object_store.commit(aas)
return response_t()
def delete_aas(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response:
@@ -580,6 +612,7 @@ def put_aas_asset_information(
) -> Response:
aas = self._get_shell(url_args)
aas.asset_information = HTTPApiDecoder.request_body(request, model.AssetInformation, False)
+ self.object_store.commit(aas)
return response_t()
def get_aas_submodel_refs(
@@ -598,6 +631,7 @@ def post_aas_submodel_refs(self, request: Request, url_args: Dict, response_t: T
if sm_ref in aas.submodel:
raise Conflict(f"{sm_ref!r} already exists!")
aas.submodel.add(sm_ref)
+ self.object_store.commit(aas)
created_resource_url = map_adapter.build(self.delete_aas_submodel_refs_specific, {
"aas_id": aas.id,
"submodel_id": sm_ref.key[0].value
@@ -609,6 +643,7 @@ def delete_aas_submodel_refs_specific(
) -> Response:
aas = self._get_shell(url_args)
aas.submodel.remove(self._get_submodel_reference(aas, url_args["submodel_id"]))
+ self.object_store.commit(aas)
return response_t()
def put_aas_submodel_refs_submodel(
@@ -622,9 +657,11 @@ def put_aas_submodel_refs_submodel(
id_changed: bool = submodel.id != new_submodel.id
# TODO: https://github.com/eclipse-basyx/basyx-python-sdk/issues/216
submodel.update_from(new_submodel)
+ self.object_store.commit(submodel)
if id_changed:
aas.submodel.remove(sm_ref)
aas.submodel.add(model.ModelReference.from_referable(submodel))
+ self.object_store.commit(aas)
return response_t()
def delete_aas_submodel_refs_submodel(
@@ -635,6 +672,7 @@ def delete_aas_submodel_refs_submodel(
submodel = self._resolve_reference(sm_ref)
self.object_store.remove(submodel)
aas.submodel.remove(sm_ref)
+ self.object_store.commit(aas)
return response_t()
def aas_submodel_refs_redirect(
@@ -711,6 +749,7 @@ def get_submodels_reference(
def put_submodel(self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs) -> Response:
submodel = self._get_submodel(url_args)
submodel.update_from(HTTPApiDecoder.request_body(request, model.Submodel, is_stripped_request(request)))
+ self.object_store.commit(submodel)
return response_t()
def get_submodel_submodel_elements(
@@ -778,6 +817,7 @@ def post_submodel_submodel_elements_id_short_path(
raise Conflict(
f"SubmodelElement with idShort {new_submodel_element.id_short} already exists " f"within {parent}!"
)
+ self.object_store.commit(self._get_submodel(url_args))
submodel = self._get_submodel(url_args)
id_short_path = url_args.get("id_shorts", [])
created_resource_url = map_adapter.build(
@@ -790,21 +830,25 @@ def post_submodel_submodel_elements_id_short_path(
def put_submodel_submodel_elements_id_short_path(
self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
) -> Response:
- submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args)
+ submodel = self._get_submodel(url_args)
+ submodel_element = self._get_nested_submodel_element(submodel, url_args["id_shorts"])
# TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T]
# see https://github.com/python/mypy/issues/5374
new_submodel_element = HTTPApiDecoder.request_body(
request, model.SubmodelElement, is_stripped_request(request) # type: ignore[type-abstract]
)
submodel_element.update_from(new_submodel_element)
+ self.object_store.commit(submodel)
return response_t()
def delete_submodel_submodel_elements_id_short_path(
self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs
) -> Response:
- sm_or_se = self._get_submodel_or_nested_submodel_element(url_args)
+ submodel = self._get_submodel(url_args)
+ sm_or_se = self._get_nested_submodel_element(submodel, url_args["id_shorts"])
parent: model.UniqueIdShortNamespace = self._expect_namespace(sm_or_se.parent, sm_or_se.id_short)
self._namespace_submodel_element_op(parent, parent.remove_referable, sm_or_se.id_short)
+ self.object_store.commit(submodel)
return response_t()
def get_submodel_submodel_element_attachment(self, request: Request, url_args: Dict, **_kwargs) -> Response:
@@ -857,6 +901,7 @@ def put_submodel_submodel_element_attachment(
)
submodel_element.value = self.file_store.add_file(filename, file_storage.stream, submodel_element.content_type)
+ self.object_store.commit(self._get_submodel(url_args))
return response_t()
def delete_submodel_submodel_element_attachment(
@@ -879,6 +924,7 @@ def delete_submodel_submodel_element_attachment(
pass
submodel_element.value = None
+ self.object_store.commit(self._get_submodel(url_args))
return response_t()
def get_submodel_submodel_element_qualifiers(
@@ -898,6 +944,7 @@ def post_submodel_submodel_element_qualifiers(
if sm_or_se.qualifier.contains_id("type", qualifier.type):
raise Conflict(f"Qualifier with type {qualifier.type} already exists!")
sm_or_se.qualifier.add(qualifier)
+ self.object_store.commit(self._get_submodel(url_args))
created_resource_url = map_adapter.build(
self.get_submodel_submodel_element_qualifiers,
{
@@ -921,6 +968,7 @@ def put_submodel_submodel_element_qualifiers(
raise Conflict(f"A qualifier of type {new_qualifier.type!r} already exists for {sm_or_se!r}")
sm_or_se.remove_qualifier_by_type(qualifier.type)
sm_or_se.qualifier.add(new_qualifier)
+ self.object_store.commit(self._get_submodel(url_args))
if qualifier_type_changed:
created_resource_url = map_adapter.build(
self.get_submodel_submodel_element_qualifiers,
@@ -940,6 +988,7 @@ def delete_submodel_submodel_element_qualifiers(
sm_or_se = self._get_submodel_or_nested_submodel_element(url_args)
qualifier_type = url_args["qualifier_type"]
self._qualifiable_qualifier_op(sm_or_se, sm_or_se.remove_qualifier_by_type, qualifier_type)
+ self.object_store.commit(self._get_submodel(url_args))
return response_t()
# --------- CONCEPT DESCRIPTION ROUTES ---------
@@ -979,6 +1028,7 @@ def put_concept_description(
concept_description.update_from(
HTTPApiDecoder.request_body(request, model.ConceptDescription, is_stripped_request(request))
)
+ self.object_store.commit(concept_description)
return response_t()
def delete_concept_description(
diff --git a/server/app/model/provider.py b/server/app/model/provider.py
index 97067e7d..8bef91e5 100644
--- a/server/app/model/provider.py
+++ b/server/app/model/provider.py
@@ -1,10 +1,11 @@
+import json
from pathlib import Path
from typing import IO, Dict, Iterable, Iterator, Union
from basyx.aas import model
from basyx.aas.model import provider as sdk_provider
-import app.adapter as adapter
+from app import adapter
from app.model import descriptor
PathOrIO = Union[Path, IO]
@@ -51,29 +52,33 @@ def __iter__(self) -> Iterator[_DESCRIPTOR_TYPE]:
return iter(self._backend.values())
+_DESCRIPTOR_KEY_TO_CLS = (
+ ("assetAdministrationShellDescriptors", descriptor.AssetAdministrationShellDescriptor),
+ ("submodelDescriptors", descriptor.SubmodelDescriptor),
+)
+
+
def load_directory(directory: Union[Path, str]) -> DictDescriptorStore:
"""
- Create a new :class:`~basyx.aas.model.provider.DictIdentifiableStore` and use it to load Asset Administration Shell
- and Submodel files in ``AASX``, ``JSON`` and ``XML`` format from a given directory into memory. Additionally, load
- all embedded supplementary files into a new :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer`.
-
- :param directory: :class:`~pathlib.Path` or ``str`` pointing to the directory containing all Asset Administration
- Shell and Submodel files to load
- :return: Tuple consisting of a :class:`~basyx.aas.model.provider.DictIdentifiableStore` and a
- :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` containing all loaded data
- """
-
- dict_descriptor_store: DictDescriptorStore = DictDescriptorStore()
+ Load AAS/Submodel descriptor JSON files from a directory into a :class:`DictDescriptorStore`.
+ :param directory: Path to the directory containing JSON descriptor files
+ :return: Populated :class:`DictDescriptorStore`
+ """
+ store = DictDescriptorStore()
directory = Path(directory)
for file in directory.iterdir():
- if not file.is_file():
+ if not file.is_file() or file.suffix.lower() != ".json":
continue
-
- suffix = file.suffix.lower()
- if suffix == ".json":
- with open(file) as f:
- adapter.read_server_aas_json_file_into(dict_descriptor_store, f)
-
- return dict_descriptor_store
+ with open(file) as f:
+ data = json.load(f, cls=adapter.ServerAASFromJsonDecoder)
+ for key, cls in _DESCRIPTOR_KEY_TO_CLS:
+ for item in data.get(key, []):
+ if isinstance(item, cls):
+ try:
+ store.add(item)
+ except KeyError:
+ pass
+
+ return store
diff --git a/server/app/model/service_specification.py b/server/app/model/service_specification.py
index 00b4a5da..5181901a 100644
--- a/server/app/model/service_specification.py
+++ b/server/app/model/service_specification.py
@@ -5,8 +5,11 @@
class ServiceSpecificationProfileEnum(str, enum.Enum):
"""
Enumeration of all standardized Service Specification Profiles
- from the AAS Part 2 API Specification (IDTA-01002-3-1).
+ from the AAS Part 2 API Specification (IDTA-01002-3-1-2).
Each profile is uniquely identified by its semantic URI.
+
+ Reference: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.1.2/
+ http-rest-api/service-specifications-and-profiles.html
"""
# --- Asset Administration Shell (AAS) ---
@@ -15,8 +18,8 @@ class ServiceSpecificationProfileEnum(str, enum.Enum):
# --- Submodel ---
SUBMODEL_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-001"
- SUBMODEL_VALUE = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-002"
- SUBMODEL_READ = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-003"
+ SUBMODEL_READ = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-002"
+ SUBMODEL_VALUE = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-003"
# --- AASX File Server ---
AASX_FILESERVER_FULL = "https://admin-shell.io/aas/API/3/1/AasxFileServerServiceSpecification/SSP-001"
@@ -28,32 +31,40 @@ class ServiceSpecificationProfileEnum(str, enum.Enum):
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-002"
AAS_REGISTRY_BULK = \
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-003"
+ AAS_REGISTRY_QUERY = \
+ "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-004"
+ AAS_REGISTRY_MINIMAL_READ = \
+ "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-005"
# --- Submodel Registry ---
SUBMODEL_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-001"
SUBMODEL_REGISTRY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-002"
SUBMODEL_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-003"
+ SUBMODEL_REGISTRY_QUERY = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-004"
# --- AAS Repository ---
AAS_REPOSITORY_FULL = \
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-001"
AAS_REPOSITORY_READ = \
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-002"
- AAS_REPOSITORY_BULK = \
+ AAS_REPOSITORY_QUERY = \
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-003"
# --- Submodel Repository ---
SUBMODEL_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-001"
SUBMODEL_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-002"
- SUBMODEL_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003"
+ SUBMODEL_REPOSITORY_TEMPLATE = \
+ "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003"
+ SUBMODEL_REPOSITORY_TEMPLATE_READ = \
+ "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-004"
+ SUBMODEL_REPOSITORY_QUERY = \
+ "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-005"
# --- Concept Description Repository ---
CONCEPT_DESCRIPTION_REPOSITORY_FULL = \
"https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001"
- CONCEPT_DESCRIPTION_REPOSITORY_READ = \
+ CONCEPT_DESCRIPTION_REPOSITORY_QUERY = \
"https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-002"
- CONCEPT_DESCRIPTION_REPOSITORY_BULK = \
- "https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-003"
# --- Discovery ---
DISCOVERY_FULL = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-001"
diff --git a/server/test/interfaces/test_shells_asset_ids.py b/server/test/interfaces/test_shells_asset_ids.py
index 8b48d6de..da103807 100644
--- a/server/test/interfaces/test_shells_asset_ids.py
+++ b/server/test/interfaces/test_shells_asset_ids.py
@@ -16,6 +16,8 @@
from app.interfaces.repository import WSGIApp
+BASE_PATH = "/api/v3.1"
+
def _encode_asset_id(name: str, value: str) -> str:
payload = json.dumps({"name": name, "value": value})
@@ -24,10 +26,24 @@ def _encode_asset_id(name: str, value: str) -> str:
class ShellsAssetIdsTest(unittest.TestCase):
def setUp(self) -> None:
- app = WSGIApp(create_full_example(), DictSupplementaryFileContainer())
+ self.example_data = create_full_example()
+ app = WSGIApp(self.example_data, DictSupplementaryFileContainer())
self.client = Client(app)
+ def test_multiple_global_asset_ids_returns_matching_results(self) -> None:
+ aas_list = [obj for obj in self.example_data if isinstance(obj, model.AssetAdministrationShell)]
+ known_id = aas_list[0].asset_information.global_asset_id
+ assert known_id is not None
+ unknown_id = "http://example.org/nonexistent_asset"
+ id1 = _encode_asset_id("globalAssetId", known_id)
+ id2 = _encode_asset_id("globalAssetId", unknown_id)
+ response = self.client.get(f"{BASE_PATH}/shells?assetIds={id1}&assetIds={id2}")
+ self.assertEqual(200, response.status_code)
+ result = json.loads(response.data)
+ returned_ids = [r["id"] for r in result]
+ self.assertIn(aas_list[0].id, returned_ids)
+
def test_malformed_asset_id_missing_field_returns_400(self) -> None:
bad_payload = base64.urlsafe_b64encode(b'{"name": "globalAssetId"}').decode()
- response = self.client.get(f"/api/v3.1/shells?assetIds={bad_payload}")
+ response = self.client.get(f"{BASE_PATH}/shells?assetIds={bad_payload}")
self.assertEqual(400, response.status_code)