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/local_file.py b/sdk/basyx/aas/backend/local_file.py index 4008497a..1c5e5971 100644 --- a/sdk/basyx/aas/backend/local_file.py +++ b/sdk/basyx/aas/backend/local_file.py @@ -110,6 +110,22 @@ 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 an updated :class:`~basyx.aas.model.base.Identifiable` object back to its storage file. + + Use this after mutating an object that was retrieved from the store to persist the changes to disk. + + :param x: The object to persist + :raises KeyError: If the object does not exist in the database + """ + logger.debug("Committing object %s to Local File Store ...", repr(x)) + path = "{}/{}.json".format(self.directory_path, self._transform_id(x.id)) + if not os.path.exists(path): + raise KeyError("No AAS object with id {} exists in local file database".format(x.id)) + with open(path, "w") as file: + json.dump({"data": x}, file, cls=json_serialization.AASToJsonEncoder, indent=4) + def discard(self, x: model.Identifiable) -> None: """ Delete an :class:`~basyx.aas.model.base.Identifiable` AAS object from the local file store @@ -150,7 +166,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 +177,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/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..f1080240 100644 --- a/sdk/test/backend/test_local_file.py +++ b/sdk/test/backend/test_local_file.py @@ -107,6 +107,33 @@ 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_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/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/interfaces/repository.py b/server/app/interfaces/repository.py index 0e75eedd..c7db5a33 100644 --- a/server/app/interfaces/repository.py +++ b/server/app/interfaces/repository.py @@ -8,9 +8,10 @@ This module implements the "Specification of the Asset Administration Shell Part 2 Application Programming Interfaces". """ +import base64 import io import json -from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Type, Union import werkzeug.exceptions import werkzeug.routing @@ -124,14 +125,14 @@ def __init__( Rule("/", methods=["GET"], endpoint=self.get_submodel), Rule("/", methods=["PUT"], endpoint=self.put_submodel), Rule("/", methods=["DELETE"], endpoint=self.delete_submodel), - Rule("/", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/", methods=["PATCH"], endpoint=self.patch_submodel), Submount( "/", [ Rule("/$metadata", methods=["GET"], endpoint=self.get_submodels_metadata), - Rule("/$metadata", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$metadata", methods=["PATCH"], endpoint=self.patch_submodel_metadata), Rule("/$value", methods=["GET"], endpoint=self.not_implemented), - Rule("/$value", methods=["PATCH"], endpoint=self.not_implemented), + Rule("/$value", methods=["PATCH"], endpoint=self.patch_submodel_value), Rule("/$reference", methods=["GET"], endpoint=self.get_submodels_reference), Rule("/$path", methods=["GET"], endpoint=self.not_implemented), Rule( @@ -182,7 +183,7 @@ def __init__( Rule( "/", methods=["PATCH"], - endpoint=self.not_implemented, + endpoint=self.patch_submodel_submodel_elements_id_short_path, ), Submount( "/", @@ -195,7 +196,7 @@ def __init__( Rule( "/$metadata", methods=["PATCH"], - endpoint=self.not_implemented, + endpoint=self.patch_submodel_submodel_elements_id_short_path_metadata, # noqa: E501 ), Rule( "/$reference", @@ -204,7 +205,8 @@ def __init__( ), Rule("/$value", methods=["GET"], endpoint=self.not_implemented), Rule( - "/$value", methods=["PATCH"], endpoint=self.not_implemented + "/$value", methods=["PATCH"], + endpoint=self.patch_submodel_submodel_elements_id_short_path_value, # noqa: E501 ), Rule("/$path", methods=["GET"], endpoint=self.not_implemented), Rule( @@ -470,10 +472,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, ) @@ -711,6 +710,8 @@ 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))) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) return response_t() def get_submodel_submodel_elements( @@ -790,6 +791,7 @@ 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 = self._get_submodel(url_args) submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) # TODO: remove the following type: ignore comment when mypy supports abstract types for Type[T] # see https://github.com/python/mypy/issues/5374 @@ -797,8 +799,116 @@ def put_submodel_submodel_elements_id_short_path( request, model.SubmodelElement, is_stripped_request(request) # type: ignore[type-abstract] ) submodel_element.update_from(new_submodel_element) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def patch_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))) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def patch_submodel_metadata( + 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, True)) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def patch_submodel_value( + self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs + ) -> Response: + submodel = self._get_submodel(url_args) + value_data = json.loads(request.get_data()) + if not isinstance(value_data, dict): + raise BadRequest("ValueOnly body for a submodel must be a JSON object") + for id_short, child_value in value_data.items(): + child = next((e for e in submodel.submodel_element if e.id_short == id_short), None) + if child is None: + raise BadRequest(f"No submodel element with idShort {id_short!r}") + self._apply_value_only(child, child_value) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def patch_submodel_submodel_elements_id_short_path( + self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs + ) -> Response: + submodel = self._get_submodel(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + new_submodel_element = HTTPApiDecoder.request_body( + request, model.SubmodelElement, is_stripped_request(request) # type: ignore[type-abstract] + ) + submodel_element.update_from(new_submodel_element) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) return response_t() + def patch_submodel_submodel_elements_id_short_path_metadata( + self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs + ) -> Response: + submodel = self._get_submodel(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + new_submodel_element = HTTPApiDecoder.request_body( + request, model.SubmodelElement, True # type: ignore[type-abstract] + ) + submodel_element.update_from(new_submodel_element) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def patch_submodel_submodel_elements_id_short_path_value( + self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs + ) -> Response: + submodel = self._get_submodel(url_args) + submodel_element = self._get_submodel_submodel_elements_id_short_path(url_args) + value_data = json.loads(request.get_data()) + self._apply_value_only(submodel_element, value_data) + if hasattr(self.object_store, "commit"): + self.object_store.commit(submodel) + return response_t() + + def _apply_value_only(self, element: model.SubmodelElement, value_data: Any) -> None: + if isinstance(element, model.Property): + element.value = ( + model.datatypes.from_xsd(str(value_data), element.value_type) + if value_data is not None else None + ) + elif isinstance(element, model.MultiLanguageProperty): + element.value = ( + model.MultiLanguageTextType({item["language"]: item["text"] for item in value_data}) + if value_data is not None else None + ) + elif isinstance(element, model.Range): + element.min = model.datatypes.from_xsd(str(value_data["min"]), element.value_type) + element.max = model.datatypes.from_xsd(str(value_data["max"]), element.value_type) + elif isinstance(element, model.Blob): + element.value = base64.b64decode(value_data) if value_data is not None else None + elif isinstance(element, model.File): + element.value = value_data + elif isinstance(element, model.SubmodelElementCollection): + if not isinstance(value_data, dict): + raise BadRequest("ValueOnly for SubmodelElementCollection must be a JSON object") + for id_short, child_value in value_data.items(): + child = next((e for e in element.value if e.id_short == id_short), None) + if child is None: + raise BadRequest(f"No submodel element with idShort {id_short!r} in {element.id_short!r}") + self._apply_value_only(child, child_value) + elif isinstance(element, model.SubmodelElementList): + if not isinstance(value_data, list): + raise BadRequest("ValueOnly for SubmodelElementList must be a JSON array") + elements = list(element.value) + for i, child_value in enumerate(value_data): + if i >= len(elements): + raise BadRequest(f"Index {i} out of range for SubmodelElementList {element.id_short!r}") + self._apply_value_only(elements[i], child_value) + else: + raise BadRequest(f"ValueOnly PATCH not supported for {type(element).__name__}") + def delete_submodel_submodel_elements_id_short_path( self, request: Request, url_args: Dict, response_t: Type[APIResponse], **_kwargs ) -> Response: diff --git a/server/app/model/provider.py b/server/app/model/provider.py index 97067e7d..472f0997 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] @@ -53,27 +54,30 @@ def __iter__(self) -> Iterator[_DESCRIPTOR_TYPE]: 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 item in data.get("assetAdministrationShellDescriptors", []): + if isinstance(item, descriptor.AssetAdministrationShellDescriptor): + try: + store.add(item) + except KeyError: + pass + for item in data.get("submodelDescriptors", []): + if isinstance(item, descriptor.SubmodelDescriptor): + 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)