Skip to content

Commit f8985e1

Browse files
committed
Merge remote-tracking branch 'origin/develop' into experimental/registry_and_discovery_server
2 parents 11669cf + ef46fd8 commit f8985e1

33 files changed

Lines changed: 325 additions & 177 deletions

File tree

.github/workflows/ci.yml

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ jobs:
292292

293293
defaults:
294294
run:
295-
working-directory: ./server/app
295+
working-directory: ./server
296296
steps:
297297
- uses: actions/checkout@v4
298298
- name: Set up Python ${{ env.X_PYTHON_MIN_VERSION }}
@@ -302,26 +302,26 @@ jobs:
302302
- name: Install Python dependencies
303303
run: |
304304
python -m pip install --upgrade pip
305-
python -m pip install ../../sdk
305+
python -m pip install ../sdk
306306
python -m pip install .[dev]
307307
- name: Check typing with MyPy
308308
run: |
309-
python -m mypy .
309+
python -m mypy app test
310310
- name: Check code style with PyCodestyle
311311
run: |
312-
python -m pycodestyle --count --max-line-length 120 .
312+
python -m pycodestyle --count --max-line-length 120 app test
313313
314-
server-package:
314+
server-repository-docker:
315315
# This job checks if we can build our server package
316316
runs-on: ubuntu-latest
317317
defaults:
318318
run:
319-
working-directory: ./server
319+
working-directory: ./server/docker/repository
320320
steps:
321321
- uses: actions/checkout@v4
322322
- name: Build the Docker image
323323
run: |
324-
docker build -t basyx-python-server -f Dockerfile ..
324+
docker build -t basyx-python-server -f Dockerfile ../../..
325325
- name: Run container
326326
run: |
327327
docker run -d --name basyx-python-server basyx-python-server

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019-2022 the Eclipse BaSyx Authors
3+
Copyright (c) 2019-2026 the Eclipse BaSyx Authors
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

compliance_tool/aas_compliance_tool/compliance_check_xml.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 the Eclipse BaSyx Authors
1+
# Copyright (c) 2026 the Eclipse BaSyx Authors
22
#
33
# This program and the accompanying materials are made available under the terms of the MIT License, available in
44
# the LICENSE file of this project.

etc/scripts/set_copyright_year.sh

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
# Usage: ./set_copyright_year.sh [PATHS]
33
#
44
# This is a small script for setting the correct copyright year
5-
# for each given file (i.e. the year the file was last changed).
6-
# Instead of file paths you can also specify directories, in which
7-
# case the script will attempt to set the copyright year for all
8-
# files in the given directories. Globbing is also possible.
5+
# for each given source file (i.e. the year the file was last
6+
# changed) and LICENSE file (i.e. the latest modification year
7+
# across all files). Instead of file paths you can also specify
8+
# directories, in which case the script will attempt to set the
9+
# copyright year for all files in the given directories.
10+
# Globbing is also possible.
911
#
10-
# The script will check the first two lines for a copyright
11-
# notice (in case the first line is a shebang).
12+
# In source files, the script checks the first two lines for a
13+
# copyright notice (in case the first line is a shebang).
14+
# In the LICENSE file, it checks the first three lines for a
15+
# copyright notice containing a year range.
1216
#
1317
# Run this script with --check to have it raise an error if it
1418
# would change anything.
@@ -17,32 +21,56 @@
1721
EXIT_CODE=0
1822

1923
# Set CHECK_MODE based on whether --check is passed
20-
CHECK_MODE=false
21-
if [[ "$1" == "--check" ]]; then
22-
CHECK_MODE=true
23-
shift # Remove --check from the arguments
24-
fi
24+
CHECK_MODE=false
25+
if [[ "$1" == "--check" ]]; then
26+
CHECK_MODE=true
27+
shift # Remove --check from the arguments
28+
fi
29+
30+
# Initialise a variable to track the latest modification year across all files
31+
max_year=""
2532

33+
# Validate the copyright year of each source file
2634
while read -rd $'\0' year file; do
2735

36+
# Extract the current modification year and update variable
37+
if [[ -z "$max_year" || "$year" -gt "$max_year" ]]; then
38+
max_year="$year"
39+
fi
40+
2841
# Extract the first year from the copyright notice
2942
current_year=$(sed -n '1,2s/^\(# Copyright (c) \)\([[:digit:]]\{4,\}\).*/\2/p' "$file")
3043

31-
# Skip the file if no year is found
44+
# Skip the source file if no year is found
3245
if [[ -z "$current_year" ]]; then
3346
continue
3447
fi
3548

49+
# If in check mode, report the incorrect copyright year
3650
if $CHECK_MODE && [[ "$current_year" != "$year" ]]; then
3751
echo "Error: Copyright year mismatch in file $file. Expected $year, found $current_year."
38-
# Set ERROR_CODE to 1 to indicate mismatch
39-
ERROR_CODE=1
52+
# Set EXIT_CODE to 1 to indicate mismatch
53+
EXIT_CODE=1
4054
fi
4155

56+
# Otherwise rewrite the incorrect copyright year
4257
if ! $CHECK_MODE && [[ "$current_year" != "$year" ]]; then
4358
sed -i "1,2s/^\(# Copyright (c) \)[[:digit:]]\{4,\}/\1$year/" "$file"
4459
echo "Updated copyright year in $file"
4560
fi
4661
done < <(git ls-files -z "$@" | xargs -0I{} git log -1 -z --format="%cd {}" --date="format:%Y" -- "{}")
4762

63+
# Validate the copyright year of the LICENSE file
64+
license_current_year=$(sed -n '1,3{s/^Copyright (c) [[:digit:]]\{4\}-\([[:digit:]]\{4\}\).*/\1/p}' LICENSE)
65+
66+
if $CHECK_MODE && [[ -n "$license_current_year" && "$license_current_year" != "$max_year" ]]; then
67+
echo "Error: Copyright year mismatch in file LICENSE. Expected $max_year, found $license_current_year."
68+
EXIT_CODE=1
69+
fi
70+
71+
if ! $CHECK_MODE && [[ -n "$license_current_year" && "$license_current_year" != "$max_year" ]]; then
72+
sed -i "1,3s/^\(Copyright (c) [[:digit:]]\{4\}-\)[[:digit:]]\{4\}/\1$max_year/" LICENSE
73+
echo "Updated copyright year in LICENSE"
74+
fi
75+
4876
exit $EXIT_CODE

sdk/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,8 @@ write_aas_xml_file(file='Simple_Submodel.xml', data=data)
124124
For further examples and tutorials, check out the `basyx.aas.examples`-package. Here is a quick overview:
125125

126126
* [`tutorial_create_simple_aas`](./basyx/aas/examples/tutorial_create_simple_aas.py): Create an Asset Administration Shell, including an Asset object and a Submodel
127-
* [`tutorial_storage`](./basyx/aas/examples/tutorial_storage.py): Manage a larger number of Asset Administration Shells in an IdentifiableStore and resolve references
127+
* [`tutorial_navigate_aas`](./basyx/aas/examples/tutorial_navigate_aas.py): Navigate Asset Administration Shell Submodels using IdShorts and IdShortPaths
128+
* [`tutorial_storage`](./basyx/aas/examples/tutorial_storage.py): Manage a larger number of Asset Administration Shells in an ObjectStore and resolve references
128129
* [`tutorial_serialization_deserialization`](./basyx/aas/examples/tutorial_serialization_deserialization.py): Use the JSON and XML serialization/deserialization for single objects or full standard-compliant files
129130
* [`tutorial_aasx`](./basyx/aas/examples/tutorial_aasx.py): Export Asset Administration Shells with related objects and auxiliary files to AASX package files
130131
* [`tutorial_backend_couchdb`](./basyx/aas/examples/tutorial_backend_couchdb.py): Use the *CouchDBIdentifiableStore* to manage and retrieve AAS objects in a CouchDB document database

sdk/basyx/aas/backend/local_file.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def discard(self, x: model.Identifiable) -> None:
123123
except FileNotFoundError as e:
124124
raise KeyError("No AAS object with id {} exists in local file database".format(x.id)) from e
125125
with self._object_cache_lock:
126-
del self._object_cache[x.id]
126+
self._object_cache.pop(x.id, None)
127127

128128
def __contains__(self, x: object) -> bool:
129129
"""
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env python3
2+
# This work is licensed under a Creative Commons CCZero 1.0 Universal License.
3+
# See http://creativecommons.org/publicdomain/zero/1.0/ for more information.
4+
"""
5+
Tutorial for navigating a Submodel's hierarchy using IdShorts and IdShortPaths.
6+
"""
7+
8+
from basyx.aas import model
9+
from typing import cast
10+
11+
# In this tutorial, you will learn how to create a Submodel with different kinds of SubmodelElements and how to navigate
12+
# through them using IdShorts and IdShortPaths.
13+
#
14+
# Step-by-Step Guide:
15+
# Step 1: Create a Submodel with a Property, a SubmodelElementCollection of Properties, a SubmodelElementList of
16+
# Properties and a SubmodelElementList of SubmodelElementCollections
17+
#
18+
# Submodel "https://iat.rwth-aachen.de/Simple_Submodel"
19+
# ├── Property "MyProperty"
20+
# │
21+
# ├── SubmodelElementCollection "MyPropertyCollection"
22+
# │ ├── Property "MyProperty0"
23+
# │ └── Property "MyProperty1"
24+
# │
25+
# ├── SubmodelElementList "MyPropertyList"
26+
# │ ├── Property [0]
27+
# │ └── Property [1]
28+
# │
29+
# └── SubmodelElementList "MyCollectionList"
30+
# ├── SubmodelElementCollection [0]
31+
# │ └── Property "MyProperty"
32+
# ├── SubmodelElementCollection [1]
33+
# │ └── Property "MyProperty"
34+
# └── SubmodelElementCollection [2]
35+
# └── Property "MyProperty"
36+
#
37+
# Step 2: Navigate through the Submodel using IdShorts and IdShortPaths
38+
39+
40+
########################################################################
41+
# Step 1: Create a Submodel with a navigable SubmodelElement hierarchy #
42+
########################################################################
43+
44+
# Step 1.1: Create a Submodel
45+
submodel = model.Submodel(id_="https://iat.rwth-aachen.de/Simple_Submodel")
46+
47+
# Step 1.2: Add a single Property to the Submodel
48+
my_property = model.Property(
49+
id_short="MyProperty",
50+
value_type=model.datatypes.String,
51+
value="I am a simple Property"
52+
)
53+
submodel.submodel_element.add(my_property)
54+
55+
# Step 1.3: Add a SubmodelElementCollection of Properties to the Submodel
56+
my_property_collection = model.SubmodelElementCollection(
57+
id_short="MyPropertyCollection",
58+
value={
59+
model.Property(
60+
id_short="MyProperty0",
61+
value_type=model.datatypes.String,
62+
value="I am the first of two Properties within a SubmodelElementCollection"
63+
),
64+
model.Property(
65+
id_short="MyProperty1",
66+
value_type=model.datatypes.String,
67+
value="I am the second of two Properties within a SubmodelElementCollection"
68+
)
69+
}
70+
)
71+
submodel.submodel_element.add(my_property_collection)
72+
73+
# Step 1.4: Add a SubmodelElementList of Properties to the Submodel
74+
my_property_list = model.SubmodelElementList(
75+
id_short="MyPropertyList",
76+
type_value_list_element=model.Property,
77+
value_type_list_element=model.datatypes.String,
78+
order_relevant=True,
79+
value=[
80+
model.Property(
81+
id_short=None,
82+
value_type=model.datatypes.String,
83+
value="I am Property 0 within a SubmodelElementList"
84+
),
85+
model.Property(
86+
id_short=None,
87+
value_type=model.datatypes.String,
88+
value="I am Property 1 within a SubmodelElementList"
89+
)
90+
]
91+
)
92+
submodel.submodel_element.add(my_property_list)
93+
94+
# Step 1.5: Add a SubmodelElementList of SubmodelElementCollections to the Submodel
95+
my_property_collection_0 = model.SubmodelElementCollection(
96+
id_short=None,
97+
value={model.Property(
98+
id_short="MyProperty",
99+
value_type=model.datatypes.String,
100+
value="I am a simple Property within SubmodelElementCollection 0"
101+
)}
102+
)
103+
my_property_collection_1 = model.SubmodelElementCollection(
104+
id_short=None,
105+
value={model.Property(
106+
id_short="MyProperty",
107+
value_type=model.datatypes.String,
108+
value="I am a simple Property within SubmodelElementCollection 1"
109+
)}
110+
)
111+
my_property_collection_2 = model.SubmodelElementCollection(
112+
id_short=None,
113+
value={model.Property(
114+
id_short="MyProperty",
115+
value_type=model.datatypes.String,
116+
value="I am a simple Property within SubmodelElementCollection 2"
117+
)}
118+
)
119+
my_collection_list = model.SubmodelElementList(
120+
id_short="MyCollectionList",
121+
type_value_list_element=model.SubmodelElementCollection,
122+
order_relevant=True,
123+
value=[my_property_collection_0, my_property_collection_1, my_property_collection_2]
124+
)
125+
submodel.submodel_element.add(my_collection_list)
126+
127+
128+
#########################################################################
129+
# Step 2: Navigate through the Submodel using IdShorts and IdShortPaths #
130+
#########################################################################
131+
132+
# Step 2.1: Access a single Property via its IdShort
133+
my_property = cast(model.Property, submodel.get_referable("MyProperty"))
134+
print(f"my_property: id_short = {my_property.id_short}, value = {my_property.value}\n")
135+
136+
# Step 2.2: Navigate through a SubmodelElementCollection of Properties
137+
# Step 2.2.1: Access a Property within a SubmodelElementCollection step by step via its IdShort
138+
my_property_collection = cast(model.SubmodelElementCollection, submodel.get_referable("MyPropertyCollection"))
139+
my_property_collection_property_0 = cast(model.Property, my_property_collection.get_referable("MyProperty0"))
140+
print(
141+
f"my_property_collection_property_0: "
142+
f"id_short = {my_property_collection_property_0}, "
143+
f"value = {my_property_collection_property_0.value}"
144+
)
145+
146+
# Step 2.2.2: Access a Property within a SubmodelElementCollection via its IdShortPath
147+
my_property_collection_property_1 = cast(
148+
model.Property,
149+
submodel.get_referable(["MyPropertyCollection", "MyProperty1"])
150+
)
151+
print(
152+
f"my_property_collection_property_1: "
153+
f"id_short = {my_property_collection_property_1}, "
154+
f"value = {my_property_collection_property_1.value}\n"
155+
)
156+
157+
# Step 2.3: Navigate through a SubmodelElementList of Properties
158+
# Step 2.3.1: Access a Property within a SubmodelElementList step by step via its index
159+
my_property_list = cast(model.SubmodelElementList, submodel.get_referable("MyPropertyList"))
160+
my_property_list_property_0 = cast(model.Property, my_property_list.get_referable("0"))
161+
print(
162+
f"my_property_list_property_0: "
163+
f"id_short = {my_property_list_property_0}, "
164+
f"value = {my_property_list_property_0.value}"
165+
)
166+
167+
# Step 2.3.2: Access a Property within a SubmodelElementList via its IdShortPath
168+
my_property_list_property_1 = cast(model.Property, submodel.get_referable(["MyPropertyList", "1"]))
169+
print(
170+
f"my_property_list_property_1: "
171+
f"id_short = {my_property_list_property_1}, "
172+
f"value = {my_property_list_property_1.value}\n"
173+
)
174+
175+
# Step 2.4: Navigate through a SubmodelElementList of SubmodelElementCollections
176+
# Step 2.4.1: Access a Property within a SubmodelElementList of SubmodelElementCollections step by step via its index
177+
# and IdShort
178+
my_collection_list = cast(model.SubmodelElementList, submodel.get_referable("MyCollectionList"))
179+
my_collection_list_collection_0 = cast(model.SubmodelElementCollection, my_collection_list.get_referable("0"))
180+
my_collection_list_collection_0_property_0 = cast(
181+
model.Property,
182+
my_collection_list_collection_0.get_referable("MyProperty")
183+
)
184+
print(
185+
f"my_collection_list_collection_0_property_0: "
186+
f"id_short = {my_collection_list_collection_0_property_0}, "
187+
f"value = {my_collection_list_collection_0_property_0.value}"
188+
)
189+
190+
# Step 2.4.2: Access a Property within a SubmodelElementList of SubmodelElementCollections via its IdShortPath
191+
my_collection_list_collection_2_property_0 = cast(
192+
model.Property,
193+
submodel.get_referable(["MyCollectionList", "2", "MyProperty"])
194+
)
195+
print(
196+
f"my_collection_list_collection_2_property_0: "
197+
f"id_short = {my_collection_list_collection_2_property_0}, "
198+
f"value = {my_collection_list_collection_2_property_0.value}"
199+
)

sdk/basyx/aas/model/provider.py

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,20 +78,6 @@ def sync(self, other: Iterable[_VALUE], overwrite: bool) -> Tuple[int, int, int]
7878
for value in other:
7979
if value in self:
8080
if overwrite:
81-
82-
# TODO: This is a quick fix. Yes it works. The underlying problem with the subclass
83-
# `LocalFileIdentifiableStore` will be solved in a separate issue
84-
# (https://github.com/eclipse-basyx/basyx-python-sdk/issues/438).
85-
# Think of this as pythonic duct tape.
86-
#
87-
# The problem is that the `_object_cache` isn't initialised together with the
88-
# `LocalFileIdentifiableStore`, leading to an error when `discard()` is called on the empty cache.
89-
# The for-loop calls `__iter__` calls `get_identifiable_by_hash()` calls
90-
# `self._object_cache[obj.id] = obj`, adding all identifiables to the cache and therefore avoiding
91-
# the error.
92-
for element in self:
93-
pass
94-
9581
self.discard(value)
9682
self.add(value)
9783
overwritten += 1

sdk/basyx/aas/util/identification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2025 the Eclipse BaSyx Authors
1+
# Copyright (c) 2026 the Eclipse BaSyx Authors
22
#
33
# This program and the accompanying materials are made available under the terms of the MIT License, available in
44
# the LICENSE file of this project.

0 commit comments

Comments
 (0)