Skip to content

Commit 2d02190

Browse files
authored
Merge branch 'develop' into update/Metamodel_V3.1.2
2 parents f667a98 + 148bd85 commit 2d02190

9 files changed

Lines changed: 244 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ jobs:
5353
# that you want to use to test the serialization adapter against.
5454
# Currently, we need to update this manually, however I'm afraid this is not possible to automatically infer,
5555
# since it's heavily dependant of the version of the AAS specification we support.
56-
AAS_SPECS_RELEASE_TAG: "IDTA-01001-3-0-1_schemasV3.0.8"
56+
AAS_SPECS_RELEASE_TAG: "IDTA-01001-3-1-2"
5757
services:
5858
couchdb:
5959
image: couchdb:3

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,14 @@ for Industry 4.0 Systems.
77

88
These are the implemented AAS specifications of the [current SDK release](https://github.com/eclipse-basyx/basyx-python-sdk/releases/latest), which can be also found on [PyPI](https://pypi.org/project/basyx-python-sdk/):
99

10-
| Specification | Version |
11-
|---------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
12-
| Part 1: Metamodel | [v3.1.2 (01001-3-0-1)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01001/v3.1.2/index.html) |
13-
| Schemata (JSONSchema, XSD) | [v3.0.8 (IDTA-01001-3-0-1_schemasV3.0.8)](https://github.com/admin-shell-io/aas-specs/releases/tag/IDTA-01001-3-0-1_schemasV3.0.8) |
14-
| Part 2: API | [v3.0 (01002-3-0)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2023/06/IDTA-01002-3-0_SpecificationAssetAdministrationShell_Part2_API_.pdf) |
15-
| Part 3a: Data Specification IEC 61360 | [v3.0 (01003-a-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01003-a-3-0_SpecificationAssetAdministrationShell_Part3a_DataSpecification_IEC61360.pdf) |
16-
| Part 5: Package File Format (AASX) | [v3.0 (01005-3-0)](https://industrialdigitaltwin.org/wp-content/uploads/2023/04/IDTA-01005-3-0_SpecificationAssetAdministrationShell_Part5_AASXPackageFileFormat.pdf) |
10+
| Specification | Version |
11+
|---------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
12+
| Part 1: Metamodel | [v3.1.2 (01001-3-0-1)](https://industrialdigitaltwin.io/aas-specifications/IDTA-01001/v3.1.2/index.html) |
13+
| Schemata (JSONSchema, XSD) | [v3.0.8 (IDTA-01001-3-0-1_schemasV3.0.8)](https://github.com/admin-shell-io/aas-specs/releases/tag/IDTA-01001-3-0-1_schemasV3.0.8) |
14+
| Part 2: API | [v3.1.1 (01002)](https://industrialdigitaltwin.org/en/wp-content/uploads/sites/2/2025/08/IDTA-01002-3-1-1_AAS-Specification_Part2_API.pdf) |
15+
| Part 3a: Data Specification IEC 61360 | [v3.1.1 (01003-a)](https://industrialdigitaltwin.org/wp-content/uploads/2025/08/IDTA-01003-a-3-1-1_AAS-Specification_Part3a_DataSpecification.pdf) |
16+
| Part 5: Package File Format (AASX) | [v3.1 (01005)](https://industrialdigitaltwin.org/wp-content/uploads/2025/06/IDTA_01005-25-01_AAS-Specification_Part5_AASXPackageFileFormat.pdf) |
17+
1718

1819
If you need support to an older version of the specifications, please refer to our [prior releases](https://github.com/eclipse-basyx/basyx-python-sdk/releases).
1920
Each of them has a similar table at the top of the release notes.

sdk/basyx/aas/adapter/aasx.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,23 +140,31 @@ def read_into(self, object_store: model.AbstractObjectStore,
140140
:return: A set of the :class:`Identifiers <basyx.aas.model.base.Identifier>` of all
141141
:class:`~basyx.aas.model.base.Identifiable` objects parsed from the AASX file
142142
"""
143+
# Format of supported and deprecated AASX relationship URL
144+
AASX_REL_BASE = "http://admin-shell.io/aasx/relationships"
145+
AASX_REL_BASE_DEPRECATED = "http://www.admin-shell.io/aasx/relationships"
146+
RELATIONSHIP_TYPE_AASX_ORIGIN = f"{AASX_REL_BASE}/aasx-origin"
147+
RELATIONSHIP_TYPE_AASX_ORIGIN_DEPRECATED = f"{AASX_REL_BASE_DEPRECATED}/aasx-origin"
148+
143149
# Find AASX-Origin part
144150
core_rels = self.reader.get_related_parts_by_type()
145151
try:
146152
aasx_origin_part = core_rels[RELATIONSHIP_TYPE_AASX_ORIGIN][0]
147153
except IndexError as e:
148-
if core_rels.get("http://www.admin-shell.io/aasx/relationships/aasx-origin"):
154+
if core_rels.get(RELATIONSHIP_TYPE_AASX_ORIGIN_DEPRECATED):
149155
# Since there are many AASX files with this (wrong) relationship URls in the wild, we make an exception
150156
# and try to read it anyway. However, we notify the user that this may lead to data loss, since it is
151157
# highly likely that the other relationship URLs are also wrong in that file.
152158
# See also [#383](https://github.com/eclipse-basyx/basyx-python-sdk/issues/383) for the discussion.
153-
logger.warning("SPECIFICATION VIOLATED: The Relationship-URL in your AASX file "
154-
"('http://www.admin-shell.io/aasx/relationships/aasx-origin') "
155-
"is not valid, it should be 'http://admin-shell.io/aasx/relationships/aasx-origin'. "
156-
"We try to read the AASX file anyway, but this cannot guaranteed in the future,"
157-
"and the file may not be fully readable, so data losses may occur."
158-
"Please fix this and/or notify the source of the AASX.")
159-
aasx_origin_part = core_rels["http://www.admin-shell.io/aasx/relationships/aasx-origin"][0]
159+
logger.warning(
160+
"Deprecated AASX relationship URL format used: '%s'. "
161+
"The supported AASX relationship URL format is: '%s'. "
162+
"Support for the deprecated form is kept for compatibility, but data losses may occur. "
163+
"Please fix the format and notify the author of the given AASX.",
164+
RELATIONSHIP_TYPE_AASX_ORIGIN_DEPRECATED,
165+
RELATIONSHIP_TYPE_AASX_ORIGIN,
166+
)
167+
aasx_origin_part = core_rels[RELATIONSHIP_TYPE_AASX_ORIGIN_DEPRECATED][0]
160168
else:
161169
raise ValueError("Not a valid AASX file: aasx-origin Relationship is missing.") from e
162170

sdk/basyx/aas/model/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
BlobType = bytes
2929

3030
# The following string aliases are constrained by the decorator functions defined in the string_constraints module,
31-
# wherever they are used for an instance attributes.
31+
# wherever they are used for an instance's attributes.
3232
ContentType = str # any mimetype as in RFC2046
3333
Identifier = str
3434
LabelType = str

server/README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,22 @@ The files are only read, changes won't persist.
1212
Alternatively, the container can also be told to use the [Local-File Backend][2] instead, which stores Asset Administration Shells (AAS) and Submodels as individual *JSON* files and allows for persistent changes (except supplementary files, i.e. files referenced by `File` SubmodelElements).
1313
See [below](#options) on how to configure this.
1414

15+
## Docker Hub
16+
17+
Pre-built images are published to [Docker Hub][11] on every release.
18+
Pull the latest version via:
19+
```
20+
$ docker pull eclipsebasyx/basyx-python-server:latest
21+
```
22+
23+
Or pin to a specific release:
24+
```
25+
$ docker pull eclipsebasyx/basyx-python-server:2.0.1
26+
```
27+
1528
## Building
1629

17-
The container image can be built via:
30+
If you need to build the image locally (e.g. for development), run:
1831
```
1932
$ docker build -t basyx-python-server -f Dockerfile ..
2033
```
@@ -86,7 +99,7 @@ The server can also be run directly on the host system without Docker, NGINX and
8699
$ python -m app.interfaces.repository
87100
```
88101

89-
The server can be accessed at http://localhost:8080/api/v3.0/ from your host system.
102+
The server can be accessed at http://localhost:8080/api/v3.1/ from your host system.
90103

91104
## Currently Unimplemented
92105
Several features and routes are currently not supported:
@@ -123,10 +136,11 @@ This Dockerfile is inspired by the [tiangolo/uwsgi-nginx-docker][10] repository.
123136
[1]: https://github.com/eclipse-basyx/basyx-python-sdk/pull/238
124137
[2]: https://basyx-python-sdk.readthedocs.io/en/latest/backend/local_file.html
125138
[3]: https://github.com/eclipse-basyx/basyx-python-sdk
126-
[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.0.1_SSP-001
127-
[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.0.1_SSP-001
128-
[6]: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.0/index.html
139+
[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.1.1_SSP-001
140+
[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.1.1_SSP-001
141+
[6]: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.1.2/index.html
129142
[7]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/aasx.html#adapter-aasx
130143
[8]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/json.html
131144
[9]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/xml.html
132145
[10]: https://github.com/tiangolo/uwsgi-nginx-docker
146+
[11]: https://hub.docker.com/r/eclipsebasyx/basyx-python-server
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Copyright (c) 2026 the Eclipse BaSyx Authors
2+
#
3+
# This program and the accompanying materials are made available under the terms of the MIT License, available in
4+
# the LICENSE file of this project.
5+
#
6+
# SPDX-License-Identifier: MIT
7+
"""
8+
This module implements constraint functions for the listed constrained string types.
9+
All types are constrained in length (min and max).
10+
11+
.. warning::
12+
This module is intended for internal use only.
13+
14+
The following types aliased in the :mod:`~server.app.interfaces.base` module are constrained:
15+
16+
- :class:`~server.app.interfaces.base.CodeType`
17+
- :class:`~server.app.interfaces.base.ShortIdType`
18+
- :class:`~server.app.interfaces.base.LocatorType`
19+
- :class:`~server.app.interfaces.base.TextType`
20+
- :class:`~server.app.interfaces.base.SchemeType`
21+
"""
22+
23+
from typing import Callable, Type
24+
from basyx.aas.model._string_constraints import check, constrain_attr, _T
25+
26+
27+
def check_code_type(value: str, type_name: str = "CodeType") -> None:
28+
return check(value, type_name, 1, 32)
29+
30+
31+
def check_short_id_type(value: str, type_name: str = "ShortIdType") -> None:
32+
return check(value, type_name, 1, 128)
33+
34+
35+
def check_locator_type(value: str, type_name: str = "LocatorType") -> None:
36+
return check(value, type_name, 1, 2048)
37+
38+
39+
def check_text_type(value: str, type_name: str = "TextType") -> None:
40+
return check(value, type_name, 1, 2048)
41+
42+
43+
def check_scheme_type(value: str, type_name: str = "SchemeType") -> None:
44+
return check(value, type_name, 1, 128)
45+
46+
47+
def constrain_code_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
48+
return constrain_attr(pub_attr_name, check_code_type)
49+
50+
51+
def constrain_short_id_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
52+
return constrain_attr(pub_attr_name, check_short_id_type)
53+
54+
55+
def constrain_locator_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
56+
return constrain_attr(pub_attr_name, check_locator_type)
57+
58+
59+
def constrain_text_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
60+
return constrain_attr(pub_attr_name, check_text_type)
61+
62+
63+
def constrain_scheme_type(pub_attr_name: str) -> Callable[[Type[_T]], Type[_T]]:
64+
return constrain_attr(pub_attr_name, check_scheme_type)

server/app/interfaces/base.py

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from basyx.aas.adapter._generic import XML_NS_MAP
2020
from basyx.aas.adapter.xml import XMLConstructables, read_aas_xml_element, xml_serialization
2121
from basyx.aas.model import AbstractObjectStore
22+
from basyx.aas.model.datatypes import NonNegativeInteger
2223
from lxml import etree
2324
from werkzeug import Request, Response
2425
from werkzeug.exceptions import BadRequest, NotFound
@@ -28,10 +29,83 @@
2829
from app.adapter import ServerAASToJsonEncoder, ServerStrictAASFromJsonDecoder, ServerStrictStrippedAASFromJsonDecoder
2930
from app.model import AssetAdministrationShellDescriptor, AssetLink, SubmodelDescriptor
3031
from app.util.converters import base64url_decode
32+
from . import _string_constraints
33+
34+
# The following string aliases are constrained by the decorator functions defined in the string_constraints module,
35+
# wherever they are used for an instances attributes.
36+
CodeType = str
37+
ShortIdType = str
38+
LocatorType = str
39+
TextType = str
40+
SchemeType = str
3141

3242
T = TypeVar("T")
3343

3444

45+
class ServiceSpecificationProfileEnum(str, enum.Enum):
46+
"""
47+
Enumeration of all standardized Service Specification Profiles
48+
from the AAS Part 2 API Specification (IDTA-01002-3-1).
49+
Each profile is uniquely identified by its semantic URI.
50+
"""
51+
52+
# --- Asset Administration Shell (AAS) ---
53+
AAS_FULL = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellServiceSpecification/SSP-001"
54+
AAS_READ = "https://admin-shell.io/aas/API/3/1/AssetAdministrationShellServiceSpecification/SSP-002"
55+
56+
# --- Submodel ---
57+
SUBMODEL_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-001"
58+
SUBMODEL_VALUE = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-002"
59+
SUBMODEL_READ = "https://admin-shell.io/aas/API/3/1/SubmodelServiceSpecification/SSP-003"
60+
61+
# --- AASX File Server ---
62+
AASX_FILESERVER_FULL = "https://admin-shell.io/aas/API/3/1/AasxFileServerServiceSpecification/SSP-001"
63+
64+
# --- AAS Registry ---
65+
AAS_REGISTRY_FULL = \
66+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-001"
67+
AAS_REGISTRY_READ = \
68+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-002"
69+
AAS_REGISTRY_BULK = \
70+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRegistryServiceSpecification/SSP-003"
71+
72+
# --- Submodel Registry ---
73+
SUBMODEL_REGISTRY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-001"
74+
SUBMODEL_REGISTRY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-002"
75+
SUBMODEL_REGISTRY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRegistryServiceSpecification/SSP-003"
76+
77+
# --- AAS Repository ---
78+
AAS_REPOSITORY_FULL = \
79+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-001"
80+
AAS_REPOSITORY_READ = \
81+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-002"
82+
AAS_REPOSITORY_BULK = \
83+
"https://admin-shell.io/aas/API/3/1/AssetAdministrationShellRepositoryServiceSpecification/SSP-003"
84+
85+
# --- Submodel Repository ---
86+
SUBMODEL_REPOSITORY_FULL = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-001"
87+
SUBMODEL_REPOSITORY_READ = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-002"
88+
SUBMODEL_REPOSITORY_BULK = "https://admin-shell.io/aas/API/3/1/SubmodelRepositoryServiceSpecification/SSP-003"
89+
90+
# --- Concept Description Repository ---
91+
CONCEPT_DESCRIPTION_REPOSITORY_FULL = \
92+
"https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-001"
93+
CONCEPT_DESCRIPTION_REPOSITORY_READ = \
94+
"https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-002"
95+
CONCEPT_DESCRIPTION_REPOSITORY_BULK = \
96+
"https://admin-shell.io/aas/API/3/1/ConceptDescriptionRepositoryServiceSpecification/SSP-003"
97+
98+
# --- Discovery ---
99+
DISCOVERY_FULL = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-001"
100+
DISCOVERY_READ = "https://admin-shell.io/aas/API/3/1/DiscoveryServiceSpecification/SSP-002"
101+
102+
103+
# TODO: Maybe remove this in spite of spec? Too complicated structure
104+
class ServiceDescription:
105+
def __init__(self, profiles: List[ServiceSpecificationProfileEnum]):
106+
self.profiles: List[ServiceSpecificationProfileEnum] = profiles
107+
108+
35109
@enum.unique
36110
class MessageType(enum.Enum):
37111
UNDEFINED = enum.auto()
@@ -44,15 +118,16 @@ def __str__(self):
44118
return self.name.capitalize()
45119

46120

121+
@_string_constraints.constrain_code_type("code")
47122
class Message:
48123
def __init__(
49124
self,
50-
code: str,
125+
code: CodeType,
51126
text: str,
52127
message_type: MessageType = MessageType.UNDEFINED,
53128
timestamp: Optional[datetime.datetime] = None,
54129
):
55-
self.code: str = code
130+
self.code: CodeType = code
56131
self.text: str = text
57132
self.message_type: MessageType = message_type
58133
self.timestamp: datetime.datetime = (
@@ -107,7 +182,7 @@ def __init__(self, *args, content_type="application/xml", **kwargs):
107182

108183
def serialize(self, obj: ResponseData, cursor: Optional[int], stripped: bool) -> str:
109184
root_elem = etree.Element("response", nsmap=XML_NS_MAP)
110-
if cursor is not None:
185+
if cursor is not None or not (isinstance(obj, list) and not obj):
111186
root_elem.set("cursor", str(cursor))
112187
if isinstance(obj, Result):
113188
result_elem = self.result_to_xml(obj, **XML_NS_MAP)
@@ -199,19 +274,21 @@ def __call__(self, environ, start_response) -> Iterable[bytes]:
199274
return response(environ, start_response)
200275

201276
@classmethod
202-
def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T], int]:
277+
def _get_slice(cls, request: Request, iterator: Iterable[T]) -> Tuple[Iterator[T], Optional[int]]:
203278
limit_str = request.args.get("limit", default="10")
204279
cursor_str = request.args.get("cursor", default="1")
205280
try:
206-
limit, cursor = int(limit_str), int(cursor_str) - 1 # cursor is 1-indexed
207-
if limit < 0 or cursor < 0:
208-
raise ValueError
281+
limit, cursor = (NonNegativeInteger(int(limit_str)),
282+
NonNegativeInteger(int(cursor_str) - 1)) # cursor is 1-indexed
209283
except ValueError:
210284
raise BadRequest("Limit can not be negative, cursor must be positive!")
211285
start_index = cursor
212286
end_index = cursor + limit
213-
paginated_slice = itertools.islice(iterator, start_index, end_index)
214-
return paginated_slice, end_index
287+
items = list(itertools.islice(iterator, start_index, end_index + 1))
288+
has_more = len(items) > limit
289+
paginated_slice = iter(items[:limit])
290+
next_cursor = cursor + limit if has_more else None
291+
return paginated_slice, next_cursor
215292

216293
def handle_request(self, request: Request):
217294
map_adapter: MapAdapter = self.url_map.bind_to_environ(request.environ)

0 commit comments

Comments
 (0)