Skip to content

Commit d05eb2a

Browse files
moritzsommers-heppnerzrgt
authored
Refactor server start-up options (#418)
Previously, the repository server used one and the same folder for loading data during start-up and storing it persistently. This resulted in a mixture of input AAS/Submodel files (AASX, JSON and XML) and persistently stored AAS/Submodel files from the `LocalFileObjectStore` (JSON). This separates the server's input and storage into two different directories to prevent their files being mixed. Moreover, the option to overwrite existing AAS/Submodels in the storage got added and the option to persistently store data got adapted. In accordance with the new changes, the `README` and `Dockerfile` were adapted to present the changes to the end users. Fixes #404 --------- Co-authored-by: s-heppner <[email protected]> Co-authored-by: Igor Garmaev <[email protected]>
1 parent 457ee51 commit d05eb2a

File tree

8 files changed

+249
-67
lines changed

8 files changed

+249
-67
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ htmlcov/
2121
docs/build/
2222
.hypothesis/
2323

24-
# customized config files
24+
# Customized config files
2525
sdk/test/test_config.ini
2626
# Schema files needed for testing
2727
sdk/test/adapter/schemas
@@ -31,5 +31,6 @@ sdk/basyx/version.py
3131
compliance_tool/aas_compliance_tool/version.py
3232
server/app/version.py
3333

34-
# ignore the content of the server storage
34+
# Ignore the content of the server storage
35+
server/input/
3536
server/storage/

sdk/basyx/aas/adapter/__init__.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,45 @@
77
Python SDK objects to/from XML.
88
* :ref:`aasx <adapter.aasx>`: This package offers functions for reading and writing AASX-files.
99
"""
10+
11+
from basyx.aas.adapter.aasx import AASXReader, DictSupplementaryFileContainer
12+
from basyx.aas.adapter.json import read_aas_json_file_into
13+
from basyx.aas.adapter.xml import read_aas_xml_file_into
14+
from basyx.aas.model.provider import DictObjectStore
15+
from pathlib import Path
16+
from typing import Union
17+
18+
19+
def load_directory(directory: Union[Path, str]) -> tuple[DictObjectStore, DictSupplementaryFileContainer]:
20+
"""
21+
Create a new :class:`~basyx.aas.model.provider.DictObjectStore` and use it to load Asset Administration Shell and
22+
Submodel files in ``AASX``, ``JSON`` and ``XML`` format from a given directory into memory. Additionally, load all
23+
embedded supplementary files into a new :class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer`.
24+
25+
:param directory: :class:`~pathlib.Path` or ``str`` pointing to the directory containing all Asset Administration
26+
Shell and Submodel files to load
27+
:return: Tuple consisting of a :class:`~basyx.aas.model.provider.DictObjectStore` and a
28+
:class:`~basyx.aas.adapter.aasx.DictSupplementaryFileContainer` containing all loaded data
29+
"""
30+
31+
dict_object_store: DictObjectStore = DictObjectStore()
32+
file_container: DictSupplementaryFileContainer = DictSupplementaryFileContainer()
33+
34+
directory = Path(directory)
35+
36+
for file in directory.iterdir():
37+
if not file.is_file():
38+
continue
39+
40+
suffix = file.suffix.lower()
41+
if suffix == ".json":
42+
with open(file) as f:
43+
read_aas_json_file_into(dict_object_store, f)
44+
elif suffix == ".xml":
45+
with open(file) as f:
46+
read_aas_xml_file_into(dict_object_store, f)
47+
elif suffix == ".aasx":
48+
with AASXReader(file) as reader:
49+
reader.read_into(object_store=dict_object_store, file_store=file_container)
50+
51+
return dict_object_store, file_container

sdk/basyx/aas/adapter/aasx.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -871,3 +871,6 @@ def __contains__(self, item: object) -> bool:
871871

872872
def __iter__(self) -> Iterator[str]:
873873
return iter(self._name_map)
874+
875+
def __len__(self) -> int:
876+
return len(self._name_map)

sdk/basyx/aas/model/provider.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"""
1212

1313
import abc
14-
from typing import MutableSet, Iterator, Generic, TypeVar, Dict, List, Optional, Iterable, Set
14+
from typing import MutableSet, Iterator, Generic, TypeVar, Dict, List, Optional, Iterable, Set, Tuple, cast
1515

1616
from .base import Identifier, Identifiable
1717

@@ -67,7 +67,7 @@ class AbstractObjectStore(AbstractObjectProvider, MutableSet[_IT], Generic[_IT],
6767
:class:`~basyx.aas.model.base.Identifier` – allow to add and delete objects (i.e. behave like a Python set).
6868
This includes local object stores (like :class:`~.DictObjectStore`) and specific object stores
6969
(like :class:`~basyx.aas.backend.couchdb.CouchDBObjectStore` and
70-
:class `~basyx.aas.backend.local_file.LocalFileObjectStore`).
70+
:class:`~basyx.aas.backend.local_file.LocalFileObjectStore`).
7171
7272
The AbstractObjectStore inherits from the :class:`~collections.abc.MutableSet` abstract collections class and
7373
therefore implements all the functions of this class.
@@ -80,6 +80,36 @@ def update(self, other: Iterable[_IT]) -> None:
8080
for x in other:
8181
self.add(x)
8282

83+
def sync(self, other: Iterable[_IT], overwrite: bool) -> Tuple[int, int, int]:
84+
"""
85+
Merge :class:`Identifiables <basyx.aas.model.base.Identifiable>` from an
86+
:class:`~collections.abc.Iterable` into this :class:`~basyx.aas.model.provider.AbstractObjectStore`.
87+
88+
:param other: :class:`~collections.abc.Iterable` to sync with
89+
:param overwrite: Flag to overwrite existing :class:`Identifiables <basyx.aas.model.base.Identifiable>` in this
90+
:class:`~basyx.aas.model.provider.AbstractObjectStore` with updated versions from ``other``,
91+
:class:`Identifiables <basyx.aas.model.base.Identifiable>` unique to this
92+
:class:`~basyx.aas.model.provider.AbstractObjectStore` are always preserved
93+
:return: Counts of processed :class:`Identifiables <basyx.aas.model.base.Identifiable>` as
94+
``(added, overwritten, skipped)``
95+
"""
96+
97+
added, overwritten, skipped = 0, 0, 0
98+
for identifiable in other:
99+
identifiable_id = identifiable.id
100+
if identifiable_id in self:
101+
if overwrite:
102+
existing = self.get_identifiable(identifiable_id)
103+
self.discard(cast(_IT, existing))
104+
self.add(identifiable)
105+
overwritten += 1
106+
else:
107+
skipped += 1
108+
else:
109+
self.add(identifiable)
110+
added += 1
111+
return added, overwritten, skipped
112+
83113

84114
class DictObjectStore(AbstractObjectStore[_IT], Generic[_IT]):
85115
"""

server/Dockerfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,22 @@ RUN chmod +x /etc/supervisor/stop-supervisor.sh
2323

2424
# Makes it possible to use a different configuration
2525
ENV UWSGI_INI=/etc/uwsgi/uwsgi.ini
26-
# object stores aren't thread-safe yet
26+
# Object stores aren't thread-safe yet
2727
# https://github.com/eclipse-basyx/basyx-python-sdk/issues/205
2828
ENV UWSGI_CHEAPER=0
2929
ENV UWSGI_PROCESSES=1
3030
ENV NGINX_MAX_UPLOAD=1M
3131
ENV NGINX_WORKER_PROCESSES=1
3232
ENV LISTEN_PORT=80
3333
ENV CLIENT_BODY_BUFFER_SIZE=1M
34+
ENV API_BASE_PATH=/api/v3.0/
35+
36+
# Default values for the storage envs
37+
ENV INPUT=/input
38+
ENV STORAGE=/storage
39+
ENV STORAGE_PERSISTENCY=False
40+
ENV STORAGE_OVERWRITE=False
41+
VOLUME ["/input", "/storage"]
3442

3543
# Copy the entrypoint that will generate Nginx additional configs
3644
COPY server/entrypoint.sh /entrypoint.sh

server/README.md

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ The server currently implements the following interfaces:
66
- [Asset Administration Shell Repository Service][4]
77
- [Submodel Repository Service][5]
88

9-
It uses the [HTTP API][1] and the [AASX][7], [JSON][8], and [XML][9] Adapters of the [BaSyx Python SDK][3], to serve regarding files from a given directory.
9+
It uses the [HTTP API][1] and the [*AASX*][7], [*JSON*][8], and [*XML*][9] Adapters of the [BaSyx Python SDK][3], to serve regarding files from a given directory.
1010
The files are only read, changes won't persist.
1111

12-
Alternatively, the container can also be told to use the [Local-File Backend][2] instead, which stores AAS and Submodels as individual JSON files and allows for persistent changes (except supplementary files, i.e. files referenced by `File` submodel elements).
12+
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

1515
## Building
@@ -19,17 +19,20 @@ The container image can be built via:
1919
$ docker build -t basyx-python-server -f Dockerfile ..
2020
```
2121

22-
Note that when cloning this repository on Windows, Git may convert the line separators to CRLF. This breaks `entrypoint.sh` and `stop-supervisor.sh`. Ensure both files use LF line separators before building.
22+
Note that when cloning this repository on Windows, Git may convert the line separators to CRLF. This breaks [`entrypoint.sh`](entrypoint.sh) and [`stop-supervisor.sh`](stop-supervisor.sh). Ensure both files use LF line separators (`\n`) before building.
2323

2424
## Running
2525

2626
### Storage
2727

28-
The container needs to be provided with the directory `/storage` to store AAS and Submodel files: AASX, JSON, XML or JSON files of Local-File Backend.
28+
The server makes use of two directories:
2929

30-
This directory can be mapped via the `-v` option from another image or a local directory.
31-
To map the directory `storage` inside the container, `-v ./storage:/storage` can be used.
32-
The directory `storage` will be created in the current working directory, if it doesn't already exist.
30+
- **`/input`** - *start-up data*: Directory from which the server loads AAS and Submodel files in *AASX*, *JSON* or *XML* format during start-up. The server will not modify these files.
31+
- **`/storage`** - *persistent store*: Directory where all AAS and Submodels are stored as individual *JSON* files if the server is [configured](#options) for persistence. The server will modify these files.
32+
33+
The directories can be mapped via the `-v` option from another image or a local directory.
34+
To mount the host directories into the container, `-v ./input:/input -v ./storage:/storage` can be used.
35+
Both local directories `./input` and `./storage` will be created in the current working directory, if they don't already exist.
3336

3437
### Port
3538

@@ -38,31 +41,40 @@ To expose it on the host on port 8080, use the option `-p 8080:80` when running
3841

3942
### Options
4043

41-
The container can be configured via environment variables:
42-
- `API_BASE_PATH` determines the base path under which all other API paths are made available.
43-
Default: `/api/v3.0`
44-
- `STORAGE_TYPE` can be one of `LOCAL_FILE_READ_ONLY` or `LOCAL_FILE_BACKEND`:
45-
- When set to `LOCAL_FILE_READ_ONLY` (the default), the server will read and serve AASX, JSON, XML files from the storage directory.
46-
The files are not modified, all changes done via the API are only stored in memory.
47-
- When instead set to `LOCAL_FILE`, the server makes use of the [LocalFileBackend][2], where AAS and Submodels are persistently stored as JSON files.
48-
Supplementary files, i.e. files referenced by `File` submodel elements, are not stored in this case.
49-
- `STORAGE_PATH` sets the directory to read the files from *within the container*. If you bind your files to a directory different from the default `/storage`, you can use this variable to adjust the server accordingly.
44+
The container can be configured via environment variables. The most important ones are summarised below:
45+
46+
| Variable | Description | Default |
47+
|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------|
48+
| `API_BASE_PATH` | Base path under which the API is served. | `/api/v3.0/` |
49+
| `INPUT` | Path inside the container pointing to the directory from which the server takes its start-up data (*AASX*, *JSON*, *XML*). | `/input` |
50+
| `STORAGE` | Path inside the container pointing to the directory used by the server to persistently store data (*JSON*). | `/storage` |
51+
| `STORAGE_PERSISTENCY` | Flag to enable data persistence via the [LocalFileBackend][2]. AAS/Submodels are stored as *JSON* files in the directory specified by `STORAGE`. Supplementary files, i.e. files referenced by `File` SubmodelElements, are not stored. If disabled, any changes made via the API are only stored in memory. | `False` |
52+
| `STORAGE_OVERWRITE` | Flag to enable storage overwrite if `STORAGE_PERSISTENCY` is enabled. Any AAS/Submodel from the `INPUT` directory already present in the LocalFileBackend replaces its existing version. If disabled, the existing version is kept. | `False` |
53+
54+
55+
This implies the following start-up behaviour:
56+
57+
- Any AAS/Submodel found in `INPUT` is loaded during start-up.
58+
- If `STORAGE_PERSISTENCY = True`:
59+
- Any AAS/Submodel *not* present in the LocalFileBackend is added to it.
60+
- Any AAS/Submodel *already present* is skipped, unless `STORAGE_OVERWRITE = True`, in which case it is replaced.
61+
- Supplementary files (e.g., `File` SubmodelElements) are never persisted by the LocalFileBackend.
5062

5163
### Running Examples
5264

5365
Putting it all together, the container can be started via the following command:
5466
```
55-
$ docker run -p 8080:80 -v ./storage:/storage basyx-python-server
67+
$ docker run -p 8080:80 -v ./input:/input -v ./storage:/storage basyx-python-server
5668
```
5769

5870
Since Windows uses backslashes instead of forward slashes in paths, you'll have to adjust the path to the storage directory there:
5971
```
60-
> docker run -p 8080:80 -v .\storage:/storage basyx-python-server
72+
> docker run -p 8080:80 -v .\input:/input -v .\storage:/storage basyx-python-server
6173
```
6274

63-
Per default, the server will use the `LOCAL_FILE_READ_ONLY` storage type and serve the API under `/api/v3.0` and read files from `/storage`. If you want to change this, you can do so like this:
75+
By default, the server will use the standard settings described [above](#options). Those settings can be adapted in the following way:
6476
```
65-
$ docker run -p 8080:80 -v ./storage2:/storage2 -e API_BASE_PATH=/api/v3.1 -e STORAGE_TYPE=LOCAL_FILE_BACKEND -e STORAGE_PATH=/storage2 basyx-python-server
77+
$ docker run -p 8080:80 -v ./input:/input2 -v ./storage:/storage2 -e API_BASE_PATH=/api/v3.1/ -e INPUT=/input2 -e STORAGE=/storage2 -e STORAGE_PERSISTENCY=True -e STORAGE_OVERWRITE=True basyx-python-server
6678
```
6779

6880
## Building and Running the Image with Docker Compose
@@ -72,8 +84,9 @@ The container image can also be built and run via:
7284
$ docker compose up
7385
```
7486

75-
This is the exemplary `compose.yml` file for the server:
87+
An exemplary [`compose.yml`](compose.yml) file for the server is given [here](compose.yml):
7688
```yaml
89+
name: basyx-python-server
7790
services:
7891
app:
7992
build:
@@ -82,13 +95,16 @@ services:
8295
ports:
8396
- "8080:80"
8497
volumes:
98+
- ./input:/input
8599
- ./storage:/storage
100+
environment:
101+
STORAGE_PERSISTENCY: True
86102
```
87103
88-
Here files are read from `/storage` and the server can be accessed at http://localhost:8080/api/v3.0/ from your host system.
89-
To get a different setup this compose.yaml file can be adapted and expanded.
104+
Input files are read from `./input` and stored persistently under `./storage` on your host system. The server can be accessed at http://localhost:8080/api/v3.0/ from your host system.
105+
To get a different setup, the [`compose.yml`](compose.yml) file can be adapted using the options described [above](#options), similar to the third [running example](#running-examples).
90106

91-
Note that the `Dockerfile` has to be specified explicitly, as the build context must be set to the parent directory of `/server` to allow access to the local `/sdk`.
107+
Note that the `Dockerfile` has to be specified explicitly via `dockerfile: server/Dockerfile`, as the build context must be set to the parent directory of `/server` to allow access to the local `/sdk`.
92108

93109
## Running without Docker (Debugging Only)
94110

@@ -103,7 +119,7 @@ The server can also be run directly on the host system without Docker, NGINX and
103119
$ pip install ./app
104120
```
105121

106-
2. Run the server by executing the main function in [`./app/interfaces/repository.py`](./app/interfaces/repository.py) from within the current folder.
122+
2. Run the server by executing the main function in [`./app/interfaces/repository.py`](./app/interfaces/repository.py).
107123
```bash
108124
$ python -m app.interfaces.repository
109125
```
@@ -119,7 +135,7 @@ This Dockerfile is inspired by the [tiangolo/uwsgi-nginx-docker][10] repository.
119135
[3]: https://github.com/eclipse-basyx/basyx-python-sdk
120136
[4]: https://app.swaggerhub.com/apis/Plattform_i40/AssetAdministrationShellRepositoryServiceSpecification/V3.0.1_SSP-001
121137
[5]: https://app.swaggerhub.com/apis/Plattform_i40/SubmodelRepositoryServiceSpecification/V3.0.1_SSP-001
122-
[6]: https://industrialdigitaltwin.org/content-hub/aasspecifications/idta_01002-3-0_application_programming_interfaces
138+
[6]: https://industrialdigitaltwin.io/aas-specifications/IDTA-01002/v3.0/index.html
123139
[7]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/aasx.html#adapter-aasx
124140
[8]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/json.html
125141
[9]: https://basyx-python-sdk.readthedocs.io/en/latest/adapter/xml.html

0 commit comments

Comments
 (0)