aas-http-client 0.1.4__tar.gz → 0.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aas-http-client might be problematic. Click here for more details.
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/PKG-INFO +1 -1
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/__init__.py +7 -3
- aas_http_client-0.1.5/aas_http_client/client.py +567 -0
- aas_http_client-0.1.5/aas_http_client/demo/demo_process.py +74 -0
- aas_http_client-0.1.5/aas_http_client/demo/logging_handler.py +177 -0
- aas_http_client-0.1.5/aas_http_client/utilities/__init__.py +0 -0
- aas_http_client-0.1.5/aas_http_client/utilities/model_builder.py +114 -0
- aas_http_client-0.1.5/aas_http_client/wrapper/sdk_wrapper.py +272 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/PKG-INFO +1 -1
- aas_http_client-0.1.5/aas_http_client.egg-info/SOURCES.txt +18 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/pyproject.toml +1 -1
- aas_http_client-0.1.5/tests/test_client.py +21 -0
- aas_http_client-0.1.4/aas_http_client.egg-info/SOURCES.txt +0 -11
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/LICENSE +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/README.md +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/core/encoder.py +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/core/version_check.py +0 -0
- /aas_http_client-0.1.4/aas_http_client/client.py → /aas_http_client-0.1.5/aas_http_client/wrapper/python_sdk_wrapper_tmp.py +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/dependency_links.txt +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/top_level.txt +0 -0
- {aas_http_client-0.1.4 → aas_http_client-0.1.5}/setup.cfg +0 -0
|
@@ -4,13 +4,17 @@ import importlib.metadata
|
|
|
4
4
|
__copyright__ = f"Copyright (C) {datetime.now().year} :em engineering methods AG. All rights reserved."
|
|
5
5
|
__author__ = "Daniel Klein"
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
try:
|
|
8
|
+
__version__ = importlib.metadata.version(__name__)
|
|
9
|
+
except importlib.metadata.PackageNotFoundError:
|
|
10
|
+
__version__ = "0.0.0-dev"
|
|
11
|
+
|
|
8
12
|
__project__ = "aas-http-client"
|
|
9
13
|
__package__ = "aas-http-client"
|
|
10
14
|
|
|
11
15
|
from aas_http_client.core.version_check import check_for_update
|
|
12
|
-
from aas_http_client.client import create_client_by_config, create_client_by_url,
|
|
16
|
+
from aas_http_client.client import create_client_by_config, create_client_by_url, AasHttpClient
|
|
13
17
|
|
|
14
18
|
check_for_update()
|
|
15
19
|
|
|
16
|
-
__all__ = ["create_client_by_config", "create_client_by_url", "
|
|
20
|
+
__all__ = ["create_client_by_config", "create_client_by_url", "AasHttpClient"]
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
"""Client for HTTP API communication with AAS server."""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import basyx.aas.adapter.json
|
|
8
|
+
import basyx.aas.adapter.json.json_serialization as js
|
|
9
|
+
import requests
|
|
10
|
+
from basyx.aas.model import Reference, Submodel
|
|
11
|
+
from aas_http_client.core.encoder import decode_base_64
|
|
12
|
+
from pydantic import BaseModel, PrivateAttr, ValidationError
|
|
13
|
+
from requests import Session
|
|
14
|
+
from requests.auth import HTTPBasicAuth
|
|
15
|
+
from requests.models import Response
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
STATUS_CODE_200 = 200
|
|
20
|
+
STATUS_CODE_201 = 201
|
|
21
|
+
STATUS_CODE_202 = 202
|
|
22
|
+
STATUS_CODE_204 = 204
|
|
23
|
+
HEADERS = {"Content-Type": "application/json"}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def log_response_errors(response: Response):
|
|
27
|
+
"""Create error messages from the response and log them.
|
|
28
|
+
|
|
29
|
+
:param response: response
|
|
30
|
+
"""
|
|
31
|
+
result_error_messages: list[str] = []
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
response_content_dict: dict = json.loads(response.content)
|
|
35
|
+
|
|
36
|
+
if "detail" in response_content_dict:
|
|
37
|
+
detail: dict = response_content_dict.get("detail", {})
|
|
38
|
+
if "error" in detail:
|
|
39
|
+
error: str = detail.get("error", "")
|
|
40
|
+
result_error_messages.append(f"{error}")
|
|
41
|
+
else:
|
|
42
|
+
result_error_messages.append(f"{detail}")
|
|
43
|
+
|
|
44
|
+
elif "messages" in response_content_dict or "Messages" in response_content_dict:
|
|
45
|
+
messages: list = response_content_dict.get("messages", [])
|
|
46
|
+
|
|
47
|
+
if not messages:
|
|
48
|
+
messages = response_content_dict.get("Messages", [])
|
|
49
|
+
|
|
50
|
+
for message in messages:
|
|
51
|
+
if isinstance(message, dict) and "message" in message:
|
|
52
|
+
result_error_messages.append(message["message"])
|
|
53
|
+
else:
|
|
54
|
+
result_error_messages.append(str(message))
|
|
55
|
+
elif "error" in response_content_dict:
|
|
56
|
+
result_error_messages.append(response_content_dict.get("error", ""))
|
|
57
|
+
|
|
58
|
+
except json.JSONDecodeError:
|
|
59
|
+
result_error_messages.append(response.content)
|
|
60
|
+
|
|
61
|
+
logger.error(f"Status code: {response.status_code}")
|
|
62
|
+
for result_error_message in result_error_messages:
|
|
63
|
+
logger.error(result_error_message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AasHttpClient(BaseModel):
|
|
67
|
+
"""Represents a AasHttpClient to communicate with a REST API."""
|
|
68
|
+
|
|
69
|
+
base_url: str = "http://javaaasserver:5060/"
|
|
70
|
+
api_base_path: str = ""
|
|
71
|
+
username: str | None = None
|
|
72
|
+
_password: str | None = PrivateAttr(default=None)
|
|
73
|
+
https_proxy: str | None = None
|
|
74
|
+
http_proxy: str | None = None
|
|
75
|
+
time_out: int = 200
|
|
76
|
+
connection_time_out: int = 100
|
|
77
|
+
ssl_verify: bool = True
|
|
78
|
+
_session: Session = PrivateAttr(default=None)
|
|
79
|
+
|
|
80
|
+
def initialize(self, password: str):
|
|
81
|
+
"""Initialize the AasHttpClient with the given URL, username and password.
|
|
82
|
+
|
|
83
|
+
:param password: password
|
|
84
|
+
"""
|
|
85
|
+
self._password = password
|
|
86
|
+
|
|
87
|
+
if self.base_url.endswith("/"):
|
|
88
|
+
self.base_url = self.base_url[:-1]
|
|
89
|
+
|
|
90
|
+
self._session = requests.Session()
|
|
91
|
+
self._session.auth = HTTPBasicAuth(self.username, self._password)
|
|
92
|
+
self._session.verify = self.ssl_verify
|
|
93
|
+
|
|
94
|
+
if self.https_proxy:
|
|
95
|
+
self._session.proxies.update({"https": self.https_proxy})
|
|
96
|
+
if self.http_proxy:
|
|
97
|
+
self._session.proxies.update({"http": self.http_proxy})
|
|
98
|
+
|
|
99
|
+
def get_root(self) -> dict | None:
|
|
100
|
+
"""Get the root of the REST API.
|
|
101
|
+
|
|
102
|
+
:return: root data as a dictionary or None if an error occurred
|
|
103
|
+
"""
|
|
104
|
+
url = f"{self.base_url}/shells"
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
response = self._session.get(url, headers=HEADERS, timeout=2)
|
|
108
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
109
|
+
|
|
110
|
+
if response.status_code != STATUS_CODE_200:
|
|
111
|
+
log_response_errors(response)
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
except requests.exceptions.RequestException as e:
|
|
115
|
+
logger.error(f"Error call REST API: {e}")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
content = response.content.decode("utf-8")
|
|
119
|
+
return json.loads(content)
|
|
120
|
+
|
|
121
|
+
def post_shells(self, aas_data: dict) -> dict | None:
|
|
122
|
+
"""Post an Asset Administration Shell (AAS) to the REST API.
|
|
123
|
+
|
|
124
|
+
:param aas_data: Json data of the Asset Administration Shell to post
|
|
125
|
+
:return: Response data as a dictionary or None if an error occurred
|
|
126
|
+
"""
|
|
127
|
+
url = f"{self.base_url}/shells"
|
|
128
|
+
logger.debug(f"Call REST API url '{url}'")
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
response = self._session.post(url, headers=HEADERS, json=aas_data, timeout=self.time_out)
|
|
132
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
133
|
+
|
|
134
|
+
if response.status_code not in (STATUS_CODE_201, STATUS_CODE_202):
|
|
135
|
+
log_response_errors(response)
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
except requests.exceptions.RequestException as e:
|
|
139
|
+
logger.error(f"Error call REST API: {e}")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
content = response.content.decode("utf-8")
|
|
143
|
+
return json.loads(content)
|
|
144
|
+
|
|
145
|
+
def put_shells(self, identifier: str, aas_data: dict) -> bool:
|
|
146
|
+
"""Update an Asset Administration Shell (AAS) by its ID in the REST API.
|
|
147
|
+
|
|
148
|
+
:param identifier: Identifier of the AAS to update
|
|
149
|
+
:param aas_data: Json data of the Asset Administration Shell data to update
|
|
150
|
+
:return: True if the update was successful, False otherwise
|
|
151
|
+
"""
|
|
152
|
+
decoded_identifier: str = decode_base_64(identifier)
|
|
153
|
+
url = f"{self.base_url}/shells/{decoded_identifier}"
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
response = self._session.put(url, headers=HEADERS, json=aas_data, timeout=self.time_out)
|
|
157
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
158
|
+
|
|
159
|
+
if response.status_code is not STATUS_CODE_204:
|
|
160
|
+
log_response_errors(response)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
except requests.exceptions.RequestException as e:
|
|
164
|
+
logger.error(f"Error call REST API: {e}")
|
|
165
|
+
return False
|
|
166
|
+
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
def put_shells_submodels(self, aas_id: str, submodel_id: str, submodel_data: dict) -> bool:
|
|
170
|
+
"""Update a submodel by its ID for a specific Asset Administration Shell (AAS).
|
|
171
|
+
|
|
172
|
+
:param aas_id: ID of the AAS to update the submodel for
|
|
173
|
+
:param submodel_data: Json data to the Submodel to update
|
|
174
|
+
:return: True if the update was successful, False otherwise
|
|
175
|
+
"""
|
|
176
|
+
decoded_aas_id: str = decode_base_64(aas_id)
|
|
177
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
178
|
+
url = f"{self.base_url}/shells/{decoded_aas_id}/submodels/{decoded_submodel_id}"
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
response = self._session.put(url, headers=HEADERS, json=submodel_data, timeout=self.time_out)
|
|
182
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
183
|
+
|
|
184
|
+
if response.status_code != STATUS_CODE_204:
|
|
185
|
+
log_response_errors(response)
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
except requests.exceptions.RequestException as e:
|
|
189
|
+
logger.error(f"Error call REST API: {e}")
|
|
190
|
+
return False
|
|
191
|
+
|
|
192
|
+
return True
|
|
193
|
+
|
|
194
|
+
def get_shells(self) -> list[dict] | None:
|
|
195
|
+
"""Get all Asset Administration Shells (AAS) from the REST API.
|
|
196
|
+
|
|
197
|
+
:return: List of paginated Asset Administration Shells data or None if an error occurred
|
|
198
|
+
"""
|
|
199
|
+
url = f"{self.base_url}/shells"
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
203
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
204
|
+
|
|
205
|
+
if response.status_code != STATUS_CODE_200:
|
|
206
|
+
log_response_errors(response)
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
except requests.exceptions.RequestException as e:
|
|
210
|
+
logger.error(f"Error call REST API: {e}")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
content = response.content.decode("utf-8")
|
|
214
|
+
return json.loads(content)
|
|
215
|
+
|
|
216
|
+
def get_shells_by_id(self, aas_id: str) -> dict | None:
|
|
217
|
+
"""Get an Asset Administration Shell (AAS) by its ID from the REST API.
|
|
218
|
+
|
|
219
|
+
:param aas_id: ID of the AAS to retrieve
|
|
220
|
+
:return: Asset Administration Shells data or None if an error occurred
|
|
221
|
+
"""
|
|
222
|
+
decoded_aas_id: str = decode_base_64(aas_id)
|
|
223
|
+
url = f"{self.base_url}/shells/{decoded_aas_id}"
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
227
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
228
|
+
|
|
229
|
+
if response.status_code != STATUS_CODE_200:
|
|
230
|
+
log_response_errors(response)
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
except requests.exceptions.RequestException as e:
|
|
234
|
+
logger.error(f"Error call REST API: {e}")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
content = response.content.decode("utf-8")
|
|
238
|
+
return json.loads(content)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_shells_reference_by_id(self, aas_id: str) -> Reference | None:
|
|
242
|
+
decoded_aas_id: str = decode_base_64(aas_id)
|
|
243
|
+
url = f"{self.base_url}/shells/{decoded_aas_id}/$reference"
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
247
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
248
|
+
|
|
249
|
+
if response.status_code != STATUS_CODE_200:
|
|
250
|
+
log_response_errors(response)
|
|
251
|
+
return None
|
|
252
|
+
|
|
253
|
+
except requests.exceptions.RequestException as e:
|
|
254
|
+
logger.error(f"Error call REST API: {e}")
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
ref_dict_string = response.content.decode("utf-8")
|
|
258
|
+
return json.loads(ref_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
259
|
+
|
|
260
|
+
def get_shells_submodels(self, aas_id: str, submodel_id: str) -> Submodel | None:
|
|
261
|
+
"""Get a submodel by its ID for a specific Asset Administration Shell (AAS).
|
|
262
|
+
|
|
263
|
+
:param aas_id: ID of the AAS to retrieve the submodel from
|
|
264
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
265
|
+
:return: Submodel object or None if an error occurred
|
|
266
|
+
"""
|
|
267
|
+
decoded_aas_id: str = decode_base_64(aas_id)
|
|
268
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
269
|
+
|
|
270
|
+
url = f"{self.base_url}/shells/{decoded_aas_id}/submodels/{decoded_submodel_id}"
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
274
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
275
|
+
|
|
276
|
+
if response.status_code != STATUS_CODE_200:
|
|
277
|
+
log_response_errors(response)
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
except requests.exceptions.RequestException as e:
|
|
281
|
+
logger.error(f"Error call REST API: {e}")
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
content = response.content.decode("utf-8")
|
|
285
|
+
return json.loads(content)
|
|
286
|
+
|
|
287
|
+
def delete_shells_by_id(self, aas_id: str) -> bool:
|
|
288
|
+
"""Get an Asset Administration Shell (AAS) by its ID from the REST API.
|
|
289
|
+
|
|
290
|
+
:param aas_id: ID of the AAS to retrieve
|
|
291
|
+
:return: True if the deletion was successful, False otherwise
|
|
292
|
+
"""
|
|
293
|
+
decoded_aas_id: str = decode_base_64(aas_id)
|
|
294
|
+
url = f"{self.base_url}/shells/{decoded_aas_id}"
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
response = self._session.delete(url, headers=HEADERS, timeout=self.time_out)
|
|
298
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
299
|
+
|
|
300
|
+
if response.status_code != STATUS_CODE_204:
|
|
301
|
+
log_response_errors(response)
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
except requests.exceptions.RequestException as e:
|
|
305
|
+
logger.error(f"Error call REST API: {e}")
|
|
306
|
+
return False
|
|
307
|
+
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
def post_submodels(self, submodel_data: dict) -> bool:
|
|
311
|
+
"""Post a submodel to the REST API.
|
|
312
|
+
|
|
313
|
+
:param submodel_data: Json data of the Submodel to post
|
|
314
|
+
:return: Response data as a dictionary or None if an error occurred
|
|
315
|
+
"""
|
|
316
|
+
url = f"{self.base_url}/submodels"
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
response = self._session.post(url, headers=HEADERS, json=submodel_data, timeout=self.time_out)
|
|
320
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
321
|
+
|
|
322
|
+
if response.status_code not in (STATUS_CODE_201, STATUS_CODE_202):
|
|
323
|
+
log_response_errors(response)
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
except requests.exceptions.RequestException as e:
|
|
327
|
+
logger.error(f"Error call REST API: {e}")
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
def put_submodels(self, identifier: str, submodel_data: dict) -> bool:
|
|
333
|
+
"""Update a submodel by its ID in the REST API.
|
|
334
|
+
|
|
335
|
+
:param identifier: Identifier of the submodel to update
|
|
336
|
+
:param submodel_data: Json data of the Submodel to update
|
|
337
|
+
:return: True if the update was successful, False otherwise
|
|
338
|
+
"""
|
|
339
|
+
decoded_identifier: str = decode_base_64(identifier)
|
|
340
|
+
url = f"{self.base_url}/submodels/{decoded_identifier}"
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
response = self._session.put(url, headers=HEADERS, json=submodel_data, timeout=self.time_out)
|
|
344
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
345
|
+
|
|
346
|
+
if response.status_code != STATUS_CODE_204:
|
|
347
|
+
log_response_errors(response)
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
except requests.exceptions.RequestException as e:
|
|
351
|
+
logger.error(f"Error call REST API: {e}")
|
|
352
|
+
return False
|
|
353
|
+
|
|
354
|
+
return True
|
|
355
|
+
|
|
356
|
+
def get_submodel_by_id(self, submodel_id: str) -> dict | None:
|
|
357
|
+
"""Get a submodel by its ID from the REST API.
|
|
358
|
+
|
|
359
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
360
|
+
:return: Submodel object or None if an error occurred
|
|
361
|
+
"""
|
|
362
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
363
|
+
url = f"{self.base_url}/submodels/{decoded_submodel_id}"
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
367
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
368
|
+
|
|
369
|
+
if response.status_code != STATUS_CODE_200:
|
|
370
|
+
log_response_errors(response)
|
|
371
|
+
return None
|
|
372
|
+
|
|
373
|
+
except requests.exceptions.RequestException as e:
|
|
374
|
+
logger.error(f"Error call REST API: {e}")
|
|
375
|
+
return None
|
|
376
|
+
|
|
377
|
+
content = response.content.decode("utf-8")
|
|
378
|
+
return json.loads(content)
|
|
379
|
+
|
|
380
|
+
def get_submodels(self) -> list[dict] | None:
|
|
381
|
+
"""Get all submodels from the REST API.
|
|
382
|
+
|
|
383
|
+
:return: Submodel objects or None if an error occurred
|
|
384
|
+
"""
|
|
385
|
+
url = f"{self.base_url}/submodels"
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
389
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
390
|
+
|
|
391
|
+
if response.status_code != STATUS_CODE_200:
|
|
392
|
+
log_response_errors(response)
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
except requests.exceptions.RequestException as e:
|
|
396
|
+
logger.error(f"Error call REST API: {e}")
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
content = response.content.decode("utf-8")
|
|
400
|
+
return json.loads(content)
|
|
401
|
+
|
|
402
|
+
def get_submodels_by_id(self, submodel_id: str) -> dict | None:
|
|
403
|
+
"""Get a submodel by its ID from the REST API.
|
|
404
|
+
|
|
405
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
406
|
+
:return: Submodel object or None if an error occurred
|
|
407
|
+
"""
|
|
408
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
409
|
+
url = f"{self.base_url}/submodels/{decoded_submodel_id}"
|
|
410
|
+
|
|
411
|
+
try:
|
|
412
|
+
response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
|
|
413
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
414
|
+
|
|
415
|
+
if response.status_code != STATUS_CODE_200:
|
|
416
|
+
log_response_errors(response)
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
except requests.exceptions.RequestException as e:
|
|
420
|
+
logger.error(f"Error call REST API: {e}")
|
|
421
|
+
return None
|
|
422
|
+
|
|
423
|
+
content = response.content.decode("utf-8")
|
|
424
|
+
return json.loads(content)
|
|
425
|
+
|
|
426
|
+
def patch_submodel_by_id(self, submodel_id: str, submodel_data: dict):
|
|
427
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
428
|
+
url = f"{self.base_url}/submodels/{decoded_submodel_id}"
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
response = self._session.patch(url, headers=HEADERS, json=submodel_data, timeout=self.time_out)
|
|
432
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
433
|
+
|
|
434
|
+
if response.status_code != STATUS_CODE_204:
|
|
435
|
+
log_response_errors(response)
|
|
436
|
+
return False
|
|
437
|
+
|
|
438
|
+
except requests.exceptions.RequestException as e:
|
|
439
|
+
logger.error(f"Error call REST API: {e}")
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
return True
|
|
443
|
+
|
|
444
|
+
def delete_submodels_by_id(self, submodel_id: str) -> bool:
|
|
445
|
+
"""Delete a submodel by its ID from the REST API.
|
|
446
|
+
|
|
447
|
+
:param submodel_id: ID of the submodel to delete
|
|
448
|
+
:return: True if the deletion was successful, False otherwise
|
|
449
|
+
"""
|
|
450
|
+
decoded_submodel_id: str = decode_base_64(submodel_id)
|
|
451
|
+
url = f"{self.base_url}/submodels/{decoded_submodel_id}"
|
|
452
|
+
|
|
453
|
+
try:
|
|
454
|
+
response = self._session.delete(url, headers=HEADERS, timeout=self.time_out)
|
|
455
|
+
logger.debug(f"Call REST API url '{response.url}'")
|
|
456
|
+
|
|
457
|
+
if response.status_code != STATUS_CODE_204:
|
|
458
|
+
log_response_errors(response)
|
|
459
|
+
return False
|
|
460
|
+
|
|
461
|
+
except requests.exceptions.RequestException as e:
|
|
462
|
+
logger.error(f"Error call REST API: {e}")
|
|
463
|
+
return False
|
|
464
|
+
|
|
465
|
+
return True
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def create_client_by_url(
|
|
469
|
+
base_url: str,
|
|
470
|
+
api_base_path: str = "",
|
|
471
|
+
username: str = "",
|
|
472
|
+
password: str = "",
|
|
473
|
+
http_proxy: str = "",
|
|
474
|
+
https_proxy: str = "",
|
|
475
|
+
time_out: int = 200,
|
|
476
|
+
connection_time_out: int = 60,
|
|
477
|
+
ssl_verify: str = True, # noqa: FBT002
|
|
478
|
+
) -> AasHttpClient | None:
|
|
479
|
+
"""Create a AAS HTTP client from the given parameters.
|
|
480
|
+
|
|
481
|
+
:param base_url: base URL of the BaSyx server, e.g. "http://basyx_python_server:80/"_
|
|
482
|
+
:param username: username for the BaSyx server interface client, defaults to ""_
|
|
483
|
+
:param password: password for the BaSyx server interface client, defaults to ""_
|
|
484
|
+
:param http_proxy: http proxy URL, defaults to ""_
|
|
485
|
+
:param https_proxy: https proxy URL, defaults to ""_
|
|
486
|
+
:param time_out: timeout for the API calls, defaults to 200
|
|
487
|
+
:param connection_time_out: timeout for the connection to the API, defaults to 60
|
|
488
|
+
:param ssl_verify: whether to verify SSL certificates, defaults to True
|
|
489
|
+
:return: An instance of AasHttpClient initialized with the provided parameters.
|
|
490
|
+
"""
|
|
491
|
+
logger.info(f"Create BaSyx server interface client from URL '{base_url}'")
|
|
492
|
+
config_dict: dict[str, str] = {}
|
|
493
|
+
config_dict["base_url"] = base_url
|
|
494
|
+
config_dict["api_base_path"] = api_base_path
|
|
495
|
+
config_dict["username"] = username
|
|
496
|
+
config_dict["http_proxy"] = http_proxy
|
|
497
|
+
config_dict["https_proxy"] = https_proxy
|
|
498
|
+
config_dict["time_out"] = time_out
|
|
499
|
+
config_dict["connection_time_out"] = connection_time_out
|
|
500
|
+
config_dict["ssl_verify"] = ssl_verify
|
|
501
|
+
config_string = json.dumps(config_dict, indent=4)
|
|
502
|
+
return _create_client(config_string, password)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def create_client_by_config(config_file: Path, password: str = "") -> AasHttpClient | None:
|
|
506
|
+
"""Create a AAS HTTP client from the given parameters.
|
|
507
|
+
|
|
508
|
+
:param config_file: Path to the configuration file containing the BaSyx server connection settings.
|
|
509
|
+
:param password: password for the BaSyx server interface client, defaults to ""_
|
|
510
|
+
:return: An instance of HttpClient initialized with the provided parameters.
|
|
511
|
+
"""
|
|
512
|
+
logger.info(f"Create BaSyx server interface client from config file '{config_file}'")
|
|
513
|
+
if not config_file.exists():
|
|
514
|
+
config_string = "{}"
|
|
515
|
+
logger.warning(f"Server config file '{config_file}' not found. Using default config.")
|
|
516
|
+
else:
|
|
517
|
+
config_string = config_file.read_text(encoding="utf-8")
|
|
518
|
+
logger.debug(f"Server config file '{config_file}' found.")
|
|
519
|
+
|
|
520
|
+
return _create_client(config_string, password)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _create_client(config_string: str, password) -> AasHttpClient | None:
|
|
524
|
+
try:
|
|
525
|
+
connection_settings = AasHttpClient.model_validate_json(config_string)
|
|
526
|
+
client = AasHttpClient(**connection_settings.model_dump())
|
|
527
|
+
except ValidationError as ve:
|
|
528
|
+
raise ValidationError(f"Invalid BaSyx server connection file: {ve}") from ve
|
|
529
|
+
|
|
530
|
+
logger.info(
|
|
531
|
+
f"Using server configuration: '{client.base_url}' | "
|
|
532
|
+
f"API base path: '{client.api_base_path}' | "
|
|
533
|
+
f"timeout: '{client.time_out}' | "
|
|
534
|
+
f"username: '{client.username}' | "
|
|
535
|
+
f"https_proxy: '{client.https_proxy}' | "
|
|
536
|
+
f"http_proxy: '{client.http_proxy}' | "
|
|
537
|
+
f"connection_timeout: '{client.connection_time_out}'"
|
|
538
|
+
)
|
|
539
|
+
client.initialize(password)
|
|
540
|
+
|
|
541
|
+
# test the connection to the REST API
|
|
542
|
+
connected = _connect_to_api(client)
|
|
543
|
+
|
|
544
|
+
if not connected:
|
|
545
|
+
return None
|
|
546
|
+
|
|
547
|
+
return client
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _connect_to_api(client: AasHttpClient) -> bool:
|
|
551
|
+
start_time = time.time()
|
|
552
|
+
logger.debug(f"Try to connect to REST API '{client.base_url}' for {client.connection_time_out} seconds")
|
|
553
|
+
counter: int = 0
|
|
554
|
+
while True:
|
|
555
|
+
try:
|
|
556
|
+
root = client.get_root()
|
|
557
|
+
if root:
|
|
558
|
+
logger.info(f"Connected to REST API at '{client.base_url}' successfully.")
|
|
559
|
+
return True
|
|
560
|
+
except requests.exceptions.ConnectionError:
|
|
561
|
+
pass
|
|
562
|
+
if time.time() - start_time > client.connection_time_out:
|
|
563
|
+
raise TimeoutError(f"Connection to REST API timed out after {client.connection_time_out} seconds.")
|
|
564
|
+
|
|
565
|
+
counter += 1
|
|
566
|
+
logger.warning(f"Retrying connection (attempt: {counter})")
|
|
567
|
+
time.sleep(1)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import aas_http_client.utilities.model_builder as model_builder
|
|
3
|
+
from aas_http_client.client import create_client_by_config, AasHttpClient
|
|
4
|
+
from aas_http_client.wrapper.sdk_wrapper import SdkWrapper, create_wrapper_by_config
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import json
|
|
7
|
+
import basyx.aas.adapter.json
|
|
8
|
+
from basyx.aas.model import AssetAdministrationShell
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
def start():
|
|
13
|
+
"""Start the demo process."""
|
|
14
|
+
|
|
15
|
+
aas_1 = _create_shell()
|
|
16
|
+
aas_2 = _create_shell()
|
|
17
|
+
|
|
18
|
+
client = _create_client()
|
|
19
|
+
sdk_wrapper = _create_sdk_wrapper()
|
|
20
|
+
|
|
21
|
+
exist_shells = sdk_wrapper.get_shells()
|
|
22
|
+
|
|
23
|
+
for shell in exist_shells:
|
|
24
|
+
logger.warning(f"Delete shell '{shell.id}'")
|
|
25
|
+
sdk_wrapper.delete_shells_by_id(shell.id)
|
|
26
|
+
|
|
27
|
+
sdk_wrapper.post_shells(aas_1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
aas_dict_string = json.dumps(aas_2, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
31
|
+
aas_dict = json.loads(aas_dict_string)
|
|
32
|
+
client.post_shells(aas_dict)
|
|
33
|
+
|
|
34
|
+
shells = client.get_shells()
|
|
35
|
+
|
|
36
|
+
logger.info(f"Client created successfully. {shells}")
|
|
37
|
+
|
|
38
|
+
def _create_shell() -> AssetAdministrationShell:
|
|
39
|
+
# create an AAS
|
|
40
|
+
aas_short_id: str = model_builder.create_unique_short_id("poc_aas")
|
|
41
|
+
aas = model_builder.create_base_ass(aas_short_id)
|
|
42
|
+
|
|
43
|
+
# create a Submodel
|
|
44
|
+
sm_short_id: str = model_builder.create_unique_short_id("poc_sm")
|
|
45
|
+
submodel = model_builder.create_base_submodel(sm_short_id)
|
|
46
|
+
|
|
47
|
+
# add Submodel to AAS
|
|
48
|
+
model_builder.add_submodel_to_aas(aas, submodel)
|
|
49
|
+
|
|
50
|
+
return aas
|
|
51
|
+
|
|
52
|
+
def _create_client() -> AasHttpClient:
|
|
53
|
+
"""Create client for java servers."""
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
file = Path("./demo/server_config.json")
|
|
57
|
+
client = create_client_by_config(file, password="")
|
|
58
|
+
except Exception as e:
|
|
59
|
+
logger.error(f"Failed to create client for {file}: {e}")
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
return client
|
|
63
|
+
|
|
64
|
+
def _create_sdk_wrapper() -> SdkWrapper:
|
|
65
|
+
"""Create client for java servers."""
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
file = Path("./demo/server_config.json")
|
|
69
|
+
client = create_wrapper_by_config(file, password="")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.error(f"Failed to create client for {file}: {e}")
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
return client
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Logging handler.
|
|
3
|
+
|
|
4
|
+
This module contains all methods and functions to handle the logging.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import queue
|
|
10
|
+
import sys
|
|
11
|
+
import uuid
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from logging.handlers import QueueHandler
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import ClassVar
|
|
16
|
+
|
|
17
|
+
from pythonjsonlogger import jsonlogger
|
|
18
|
+
|
|
19
|
+
LOG_FOLDER: str = "./_log"
|
|
20
|
+
LOG_FILE_SUFFIX: str = "_log.json"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ColorCodes:
|
|
24
|
+
"""Define the color codes for the console output."""
|
|
25
|
+
|
|
26
|
+
grey = "\x1b[38;21m"
|
|
27
|
+
green = "\x1b[1;32m"
|
|
28
|
+
yellow = "\x1b[33;21m"
|
|
29
|
+
red = "\x1b[31;21m"
|
|
30
|
+
bold_red = "\x1b[31;1m"
|
|
31
|
+
blue = "\x1b[1;34m"
|
|
32
|
+
light_blue = "\x1b[1;36m"
|
|
33
|
+
purple = "\x1b[1;35m"
|
|
34
|
+
reset = "\x1b[0m"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CustomConsoleFormatter(logging.Formatter):
|
|
38
|
+
"""Custom console formatter for logging with colored level.
|
|
39
|
+
|
|
40
|
+
:param logging: formatter
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
FORMATS: ClassVar[dict] = {
|
|
44
|
+
logging.DEBUG: ColorCodes.blue + "%(levelname)s" + ColorCodes.reset + ": %(message)s (%(filename)s:%(lineno)d)",
|
|
45
|
+
logging.INFO: ColorCodes.green + "%(levelname)s" + ColorCodes.reset + ": %(message)s",
|
|
46
|
+
logging.WARNING: ColorCodes.yellow + "%(levelname)s" + ColorCodes.reset + ": %(message)s",
|
|
47
|
+
logging.ERROR: ColorCodes.red + "%(levelname)s" + ColorCodes.reset + ": %(message)s (%(filename)s:%(lineno)d)",
|
|
48
|
+
logging.CRITICAL: ColorCodes.bold_red + "%(levelname)s: %(message)s (%(filename)s:%(lineno)d)" + ColorCodes.reset,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
def format(self, record) -> str:
|
|
52
|
+
"""Format the log record.
|
|
53
|
+
|
|
54
|
+
:param record: record to format
|
|
55
|
+
:return: formatted record
|
|
56
|
+
"""
|
|
57
|
+
log_fmt = self.FORMATS.get(record.levelno)
|
|
58
|
+
formatter = logging.Formatter(log_fmt)
|
|
59
|
+
return formatter.format(record)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _handle_file_rotation(log_file_path: Path, max_file_count: int = 5) -> None:
|
|
63
|
+
log_folder: Path = log_file_path.resolve()
|
|
64
|
+
|
|
65
|
+
if max_file_count < 1:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
if not log_folder.exists():
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
existing_log_files: list[Path] = [file for file in log_folder.iterdir() if file.name.endswith(LOG_FILE_SUFFIX)]
|
|
72
|
+
|
|
73
|
+
if len(existing_log_files) < max_file_count:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
existing_log_files.sort(key=lambda x: x.stat().st_ctime)
|
|
77
|
+
|
|
78
|
+
files_to_delete: int = len(existing_log_files) - (max_file_count - 1)
|
|
79
|
+
|
|
80
|
+
for file in existing_log_files[:files_to_delete]:
|
|
81
|
+
file.unlink()
|
|
82
|
+
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def initialize_logging(console_level=logging.INFO) -> Path:
|
|
87
|
+
"""Initialize the standard logging.
|
|
88
|
+
|
|
89
|
+
:param debug_mode_status: Status of the debug mode
|
|
90
|
+
:param log_file_name: Name of the (path and extension)
|
|
91
|
+
"""
|
|
92
|
+
log_path = Path(LOG_FOLDER).resolve()
|
|
93
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
|
|
95
|
+
log_file_path = log_path / "api.log"
|
|
96
|
+
|
|
97
|
+
log_file_format = "%(asctime)s %(levelname)s: %(message)s (%(filename)s:%(lineno)d)"
|
|
98
|
+
logging.basicConfig(
|
|
99
|
+
filename=log_file_path,
|
|
100
|
+
level=logging.DEBUG,
|
|
101
|
+
format=log_file_format,
|
|
102
|
+
filemode="w",
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# set console logging
|
|
106
|
+
console_handler = logging.StreamHandler()
|
|
107
|
+
console_handler.setLevel(console_level)
|
|
108
|
+
console_handler.setFormatter(CustomConsoleFormatter())
|
|
109
|
+
logging.getLogger("").addHandler(console_handler)
|
|
110
|
+
|
|
111
|
+
# set queue logging
|
|
112
|
+
log_queue: queue.Queue = queue.Queue(-1) # Use default max size
|
|
113
|
+
queue_handler = QueueHandler(log_queue)
|
|
114
|
+
logging.getLogger("").addHandler(queue_handler)
|
|
115
|
+
|
|
116
|
+
logger = logging.getLogger(__name__)
|
|
117
|
+
script_path = Path(sys.argv[0])
|
|
118
|
+
python_version = sys.version.replace("\n", "")
|
|
119
|
+
|
|
120
|
+
print("")
|
|
121
|
+
logger.info(f"Run script '{script_path.name.replace('.py', '')}'")
|
|
122
|
+
logger.info(f"Script executed by Python v{python_version}")
|
|
123
|
+
logger.info("Logging initialized")
|
|
124
|
+
|
|
125
|
+
return log_file_path.resolve()
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def read_log_file_as_list(log_file_path: Path) -> list[dict]:
|
|
129
|
+
"""Read the log file as a list of dictionaries (Json conform).
|
|
130
|
+
|
|
131
|
+
:param log_file_path: Path to the log file
|
|
132
|
+
:return: list of dictionaries (Json conform)
|
|
133
|
+
"""
|
|
134
|
+
with Path.open(log_file_path, "r", encoding="utf-8") as f:
|
|
135
|
+
return [json.loads(line) for line in f if line.strip()]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def set_log_file(
|
|
139
|
+
max_log_files: int = 10,
|
|
140
|
+
) -> Path:
|
|
141
|
+
"""Set the log file.
|
|
142
|
+
|
|
143
|
+
:param max_log_files: max number of log files in folder, defaults to 5
|
|
144
|
+
:return: log file path
|
|
145
|
+
"""
|
|
146
|
+
logger = logging.getLogger() # Get the root logger
|
|
147
|
+
|
|
148
|
+
# Remove all existing file handlers
|
|
149
|
+
for handler in logger.handlers[:]:
|
|
150
|
+
if isinstance(handler, logging.FileHandler):
|
|
151
|
+
logger.removeHandler(handler)
|
|
152
|
+
handler.close()
|
|
153
|
+
|
|
154
|
+
now = datetime.now(tz=UTC)
|
|
155
|
+
time_string = now.strftime("%Y-%m-%d_%H-%M-%S")
|
|
156
|
+
|
|
157
|
+
# handle log file and folder
|
|
158
|
+
log_path: Path = Path(LOG_FOLDER).resolve()
|
|
159
|
+
log_path = Path(LOG_FOLDER, "runtime").resolve()
|
|
160
|
+
|
|
161
|
+
log_path.mkdir(parents=True, exist_ok=True)
|
|
162
|
+
log_file_name = f"{uuid.uuid4().hex}{LOG_FILE_SUFFIX}"
|
|
163
|
+
log_file_path = log_path / f"{time_string}_{log_file_name}"
|
|
164
|
+
|
|
165
|
+
_handle_file_rotation(log_path, max_log_files)
|
|
166
|
+
|
|
167
|
+
# Add a new file handler with the new log file path
|
|
168
|
+
json_formatter = jsonlogger.JsonFormatter("%(asctime)s %(levelname)s %(name)s %(message)s %(filename)s %(lineno)d")
|
|
169
|
+
json_file_handler = logging.FileHandler(log_file_path, mode="w")
|
|
170
|
+
json_file_handler.setFormatter(json_formatter)
|
|
171
|
+
json_file_handler.setLevel(logging.DEBUG)
|
|
172
|
+
logger.addHandler(json_file_handler)
|
|
173
|
+
|
|
174
|
+
logging.info(f"Maximum log file number is: {max_log_files}") # noqa: LOG015
|
|
175
|
+
logging.info(f"Write log file to: '{log_file_path}'") # noqa: LOG015
|
|
176
|
+
|
|
177
|
+
return log_file_path.resolve()
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Model builder module.
|
|
2
|
+
|
|
3
|
+
Provides some helper methods for easier work with basyx sdk data model
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
from basyx.aas.model import (
|
|
9
|
+
AssetAdministrationShell,
|
|
10
|
+
AssetInformation,
|
|
11
|
+
AssetKind,
|
|
12
|
+
Key,
|
|
13
|
+
ModelReference,
|
|
14
|
+
MultiLanguageTextType,
|
|
15
|
+
Submodel,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def create_unique_short_id(id_short: str) -> str:
|
|
20
|
+
"""Generate a unique identifier string by appending a UUID to the provided ID short.
|
|
21
|
+
|
|
22
|
+
:param id_short: provided ID short
|
|
23
|
+
:return: unique identifier
|
|
24
|
+
"""
|
|
25
|
+
return f"{id_short}_{str(uuid.uuid4()).replace('-', '_')}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def create_base_submodel(id_short: str, namespace: str = "basyx_python_aas_server", display_name: str = "", description: str = "") -> Submodel:
|
|
29
|
+
"""Create a basic Submodel.
|
|
30
|
+
|
|
31
|
+
:param id_short: ID short of the Submodel
|
|
32
|
+
:param namespace: namespace of the Submodel , defaults to "basyx_python_aas_server"
|
|
33
|
+
:param display_name: display name of the Submodel, defaults to ""
|
|
34
|
+
:param description: description of the Submodel, defaults to ""
|
|
35
|
+
:return: Submodel instance
|
|
36
|
+
"""
|
|
37
|
+
identifier = f"{namespace}/{id_short}"
|
|
38
|
+
sm = Submodel(identifier)
|
|
39
|
+
sm.id_short = id_short
|
|
40
|
+
|
|
41
|
+
if not description:
|
|
42
|
+
description = f"This is the submodel with ID short '{id_short}'"
|
|
43
|
+
|
|
44
|
+
description_text = {"en": f"{description}"}
|
|
45
|
+
sm.description = MultiLanguageTextType(description_text)
|
|
46
|
+
|
|
47
|
+
if not display_name:
|
|
48
|
+
display_name = "POC AAS"
|
|
49
|
+
|
|
50
|
+
display_name_text = {"en": f"{display_name}"}
|
|
51
|
+
sm.display_name = MultiLanguageTextType(display_name_text)
|
|
52
|
+
|
|
53
|
+
return sm
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_base_ass(
|
|
57
|
+
id_short: str, namespace: str = "basyx_python_aas_server", display_name: str = "", description: str = ""
|
|
58
|
+
) -> AssetAdministrationShell:
|
|
59
|
+
"""Create a basic AAS.
|
|
60
|
+
|
|
61
|
+
:param id_short: ID short of the AAS
|
|
62
|
+
:param namespace: namespace of the AAS, defaults to "basyx_python_aas_server"
|
|
63
|
+
:param display_name: display name of the AAS, defaults to ""
|
|
64
|
+
:param description: description of the AAS, defaults to ""
|
|
65
|
+
:return: AssetAdministrationShell instance
|
|
66
|
+
"""
|
|
67
|
+
asset_info = create_base_asset_information(id_short, namespace)
|
|
68
|
+
|
|
69
|
+
aas = AssetAdministrationShell(id_=asset_info.global_asset_id, asset_information=asset_info)
|
|
70
|
+
aas.id_short = id_short
|
|
71
|
+
|
|
72
|
+
if not description:
|
|
73
|
+
description = f"This is the asset administration shell with ID short '{id_short}'"
|
|
74
|
+
|
|
75
|
+
description_text = {"en": f"{description}"}
|
|
76
|
+
aas.description = MultiLanguageTextType(description_text)
|
|
77
|
+
|
|
78
|
+
if not display_name:
|
|
79
|
+
display_name = "POC AAS"
|
|
80
|
+
|
|
81
|
+
display_name_text = {"en": f"{display_name}"}
|
|
82
|
+
aas.display_name = MultiLanguageTextType(display_name_text)
|
|
83
|
+
|
|
84
|
+
return aas
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def add_submodel_to_aas(aas: AssetAdministrationShell, submodel: Submodel) -> None:
|
|
88
|
+
"""Add a given Submodel correctly to a provided AssetAdministrationShell.
|
|
89
|
+
|
|
90
|
+
:param aas: provided AssetAdministrationShell to which the Submodel should be added
|
|
91
|
+
:param submodel: given Submodel to add
|
|
92
|
+
"""
|
|
93
|
+
# aas.submodel.add(submodel)
|
|
94
|
+
aas.submodel.add(ModelReference.from_referable(submodel))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def create_base_asset_information(id_short: str, namespace: str = "basyx_python_aas_server") -> AssetInformation:
|
|
98
|
+
"""Return a basic AssetInformation instance.
|
|
99
|
+
|
|
100
|
+
:param id_short: short ID of the AssetInformation
|
|
101
|
+
:param namespace: namespace of the AssetInformation, defaults to "basyx_python_aas_server"
|
|
102
|
+
:return: AssetInformation instance
|
|
103
|
+
"""
|
|
104
|
+
identifier = f"{namespace}/{id_short}"
|
|
105
|
+
return AssetInformation(AssetKind.INSTANCE, identifier)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def create_reference(id: str) -> ModelReference:
|
|
109
|
+
"""Create a ModelReference.
|
|
110
|
+
|
|
111
|
+
:param id: ID of the Submodel to reference
|
|
112
|
+
:return: ModelReference instance
|
|
113
|
+
"""
|
|
114
|
+
return ModelReference.from_referable(Submodel(id))
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"""BaSyx Server interface for REST API communication."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import basyx.aas.adapter.json
|
|
8
|
+
|
|
9
|
+
from basyx.aas.model import AssetAdministrationShell, Reference, Submodel
|
|
10
|
+
from aas_http_client.client import AasHttpClient, _create_client
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class SdkWrapper():
|
|
14
|
+
"""Represents a SdkWrapper to communicate with a REST API."""
|
|
15
|
+
_client: AasHttpClient = None
|
|
16
|
+
|
|
17
|
+
def post_shells(self, aas: AssetAdministrationShell) -> dict | None:
|
|
18
|
+
"""Post an Asset Administration Shell (AAS) to the REST API.
|
|
19
|
+
|
|
20
|
+
:param aas: Asset Administration Shell to post
|
|
21
|
+
:return: Response data as a dictionary or None if an error occurred
|
|
22
|
+
"""
|
|
23
|
+
aas_data_string = json.dumps(aas, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
24
|
+
aas_data = json.loads(aas_data_string)
|
|
25
|
+
|
|
26
|
+
return self._client.post_shells(aas_data)
|
|
27
|
+
|
|
28
|
+
def put_shells(self, identifier: str, aas: AssetAdministrationShell) -> bool:
|
|
29
|
+
"""Update an Asset Administration Shell (AAS) by its ID in the REST API.
|
|
30
|
+
|
|
31
|
+
:param identifier: Identifier of the AAS to update
|
|
32
|
+
:param aas: Asset Administration Shell data to update
|
|
33
|
+
:return: True if the update was successful, False otherwise
|
|
34
|
+
"""
|
|
35
|
+
aas_data_string = json.dumps(aas, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
36
|
+
aas_data = json.loads(aas_data_string)
|
|
37
|
+
|
|
38
|
+
return self._client.put_shells(identifier, aas_data)
|
|
39
|
+
|
|
40
|
+
def put_shells_submodels(self, aas_id: str, submodel_id: str, submodel: Submodel) -> bool:
|
|
41
|
+
"""Update a submodel by its ID for a specific Asset Administration Shell (AAS).
|
|
42
|
+
|
|
43
|
+
:param aas_id: ID of the AAS to update the submodel for
|
|
44
|
+
:param submodel: Submodel data to update
|
|
45
|
+
:return: True if the update was successful, False otherwise
|
|
46
|
+
"""
|
|
47
|
+
sm_data_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
48
|
+
sm_data = json.loads(sm_data_string)
|
|
49
|
+
|
|
50
|
+
return self._client.put_shells_submodels(aas_id, submodel_id, sm_data)
|
|
51
|
+
|
|
52
|
+
def get_shells(self) -> list[AssetAdministrationShell] | None:
|
|
53
|
+
"""Get all Asset Administration Shells (AAS) from the REST API.
|
|
54
|
+
|
|
55
|
+
:return: AAS objects or None if an error occurred
|
|
56
|
+
"""
|
|
57
|
+
content: dict = self._client.get_shells()
|
|
58
|
+
|
|
59
|
+
if not content:
|
|
60
|
+
logger.warning("No AAS found in the REST API.")
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
results: list = content.get("result", [])
|
|
64
|
+
if not results:
|
|
65
|
+
logger.warning("No AAS found in the REST API results.")
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
aas_list: list[AssetAdministrationShell] = []
|
|
69
|
+
|
|
70
|
+
for result in results:
|
|
71
|
+
if not isinstance(result, dict):
|
|
72
|
+
logger.error(f"Invalid AAS data: {result}")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
aas_dict_string = json.dumps(result)
|
|
76
|
+
aas = json.loads(aas_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
77
|
+
aas_list.append(aas)
|
|
78
|
+
|
|
79
|
+
return aas_list
|
|
80
|
+
|
|
81
|
+
def get_shells_by_id(self, aas_id: str) -> AssetAdministrationShell | None:
|
|
82
|
+
"""Get an Asset Administration Shell (AAS) by its ID from the REST API.
|
|
83
|
+
|
|
84
|
+
:param aas_id: ID of the AAS to retrieve
|
|
85
|
+
:return: AAS object or None if an error occurred
|
|
86
|
+
"""
|
|
87
|
+
content: dict = self._client.get_shells_by_id(aas_id)
|
|
88
|
+
return json.load(content, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
89
|
+
|
|
90
|
+
def get_shells_reference_by_id(self, aas_id: str) -> Reference | None:
|
|
91
|
+
content: dict = self._client.get_shells_reference_by_id(aas_id)
|
|
92
|
+
return json.load(content, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
93
|
+
|
|
94
|
+
def get_shells_submodels(self, aas_id: str, submodel_id: str) -> Submodel | None:
|
|
95
|
+
"""Get a submodel by its ID for a specific Asset Administration Shell (AAS).
|
|
96
|
+
|
|
97
|
+
:param aas_id: ID of the AAS to retrieve the submodel from
|
|
98
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
99
|
+
:return: Submodel object or None if an error occurred
|
|
100
|
+
"""
|
|
101
|
+
content: dict = self._client.get_shells_by_id(aas_id)
|
|
102
|
+
return json.load(content, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
103
|
+
|
|
104
|
+
def delete_shells_by_id(self, aas_id: str) -> bool:
|
|
105
|
+
"""Get an Asset Administration Shell (AAS) by its ID from the REST API.
|
|
106
|
+
|
|
107
|
+
:param aas_id: ID of the AAS to retrieve
|
|
108
|
+
:return: True if the deletion was successful, False otherwise
|
|
109
|
+
"""
|
|
110
|
+
return self._client.delete_shells_by_id(aas_id)
|
|
111
|
+
|
|
112
|
+
def post_submodels(self, submodel: Submodel) -> bool:
|
|
113
|
+
"""Post a submodel to the REST API.
|
|
114
|
+
|
|
115
|
+
:param submodel: submodel data as a dictionary
|
|
116
|
+
:return: Response data as a dictionary or None if an error occurred
|
|
117
|
+
"""
|
|
118
|
+
sm_data_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
119
|
+
sm_data = json.loads(sm_data_string)
|
|
120
|
+
|
|
121
|
+
return self._client.post_submodels(sm_data)
|
|
122
|
+
|
|
123
|
+
def put_submodels(self, identifier: str, submodel: Submodel) -> bool:
|
|
124
|
+
"""Update a submodel by its ID in the REST API.
|
|
125
|
+
|
|
126
|
+
:param identifier: Identifier of the submodel to update
|
|
127
|
+
:param submodel: Submodel data to update
|
|
128
|
+
:return: True if the update was successful, False otherwise
|
|
129
|
+
"""
|
|
130
|
+
sm_data_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
131
|
+
sm_data = json.loads(sm_data_string)
|
|
132
|
+
|
|
133
|
+
return self._client.put_submodels(identifier, sm_data)
|
|
134
|
+
|
|
135
|
+
def get_submodel_by_id(self, submodel_id: str) -> Submodel | None:
|
|
136
|
+
"""Get a submodel by its ID from the REST API.
|
|
137
|
+
|
|
138
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
139
|
+
:return: Submodel object or None if an error occurred
|
|
140
|
+
"""
|
|
141
|
+
content = self._client.get_submodel_by_id(submodel_id)
|
|
142
|
+
|
|
143
|
+
if not content:
|
|
144
|
+
logger.warning(f"No submodel found with ID '{submodel_id}' in the REST API.")
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
if not isinstance(content, dict):
|
|
148
|
+
logger.error(f"Invalid submodel data: {content}")
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
return json.loads(content, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
152
|
+
|
|
153
|
+
def get_submodels(self) -> list[Submodel] | None:
|
|
154
|
+
"""Get all submodels from the REST API.
|
|
155
|
+
|
|
156
|
+
:return: Submodel objects or None if an error occurred
|
|
157
|
+
"""
|
|
158
|
+
content: list = self._client.get_submodels()
|
|
159
|
+
|
|
160
|
+
if not content:
|
|
161
|
+
logger.warning("No submodels found in the REST API.")
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
results: list = content.get("result", [])
|
|
165
|
+
if not results:
|
|
166
|
+
logger.warning("No submodels found in the REST API results.")
|
|
167
|
+
return []
|
|
168
|
+
|
|
169
|
+
submodels: list[Submodel] = []
|
|
170
|
+
|
|
171
|
+
for result in results:
|
|
172
|
+
if not isinstance(result, dict):
|
|
173
|
+
logger.error(f"Invalid submodel data: {result}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
sm_dict_string = json.dumps(result)
|
|
177
|
+
submodel = json.loads(sm_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
178
|
+
submodels.append(submodel)
|
|
179
|
+
|
|
180
|
+
return submodels
|
|
181
|
+
|
|
182
|
+
def get_submodels_by_id(self, submodel_id: str) -> Submodel | None:
|
|
183
|
+
"""Get a submodel by its ID from the REST API.
|
|
184
|
+
|
|
185
|
+
:param submodel_id: ID of the submodel to retrieve
|
|
186
|
+
:return: Submodel object or None if an error occurred
|
|
187
|
+
"""
|
|
188
|
+
content = self._client.get_submodels_by_id(submodel_id)
|
|
189
|
+
|
|
190
|
+
if not content:
|
|
191
|
+
logger.warning(f"No submodel found with ID '{submodel_id}' in the REST API.")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
if not isinstance(content, dict):
|
|
195
|
+
logger.error(f"Invalid submodel data: {content}")
|
|
196
|
+
return None
|
|
197
|
+
#
|
|
198
|
+
return json.loads(content, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
|
|
199
|
+
|
|
200
|
+
def patch_submodel_by_id(self, submodel_id: str, submodel: Submodel):
|
|
201
|
+
sm_dict_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
|
|
202
|
+
sm_dict = json.loads(sm_dict_string)
|
|
203
|
+
|
|
204
|
+
return self._client.patch_submodel_by_id(submodel_id, sm_dict)
|
|
205
|
+
|
|
206
|
+
def delete_submodels_by_id(self, submodel_id: str) -> bool:
|
|
207
|
+
"""Delete a submodel by its ID from the REST API.
|
|
208
|
+
|
|
209
|
+
:param submodel_id: ID of the submodel to delete
|
|
210
|
+
:return: True if the deletion was successful, False otherwise
|
|
211
|
+
"""
|
|
212
|
+
return self._client.delete_submodels_by_id(submodel_id)
|
|
213
|
+
|
|
214
|
+
def create_wrapper_by_url(
|
|
215
|
+
base_url: str,
|
|
216
|
+
api_base_path: str = "",
|
|
217
|
+
username: str = "",
|
|
218
|
+
password: str = "",
|
|
219
|
+
http_proxy: str = "",
|
|
220
|
+
https_proxy: str = "",
|
|
221
|
+
time_out: int = 200,
|
|
222
|
+
connection_time_out: int = 60,
|
|
223
|
+
ssl_verify: str = True, # noqa: FBT002
|
|
224
|
+
) -> SdkWrapper | None:
|
|
225
|
+
"""Create a BaSyx server interface client from the given parameters.
|
|
226
|
+
|
|
227
|
+
:param base_url: base URL of the BaSyx server, e.g. "http://basyx_python_server:80/"_
|
|
228
|
+
:param username: username for the BaSyx server interface client, defaults to ""_
|
|
229
|
+
:param password: password for the BaSyx server interface client, defaults to ""_
|
|
230
|
+
:param http_proxy: http proxy URL, defaults to ""_
|
|
231
|
+
:param https_proxy: https proxy URL, defaults to ""_
|
|
232
|
+
:param time_out: timeout for the API calls, defaults to 200
|
|
233
|
+
:param connection_time_out: timeout for the connection to the API, defaults to 60
|
|
234
|
+
:param ssl_verify: whether to verify SSL certificates, defaults to True
|
|
235
|
+
:return: An instance of HttpClient initialized with the provided parameters.
|
|
236
|
+
"""
|
|
237
|
+
logger.info(f"Create BaSyx server interface client from URL '{base_url}'")
|
|
238
|
+
config_dict: dict[str, str] = {}
|
|
239
|
+
config_dict["base_url"] = base_url
|
|
240
|
+
config_dict["api_base_path"] = api_base_path
|
|
241
|
+
config_dict["username"] = username
|
|
242
|
+
config_dict["http_proxy"] = http_proxy
|
|
243
|
+
config_dict["https_proxy"] = https_proxy
|
|
244
|
+
config_dict["time_out"] = time_out
|
|
245
|
+
config_dict["connection_time_out"] = connection_time_out
|
|
246
|
+
config_dict["ssl_verify"] = ssl_verify
|
|
247
|
+
config_string = json.dumps(config_dict, indent=4)
|
|
248
|
+
|
|
249
|
+
wrapper = SdkWrapper()
|
|
250
|
+
wrapper._client = _create_client(config_string, password)
|
|
251
|
+
return wrapper
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def create_wrapper_by_config(config_file: Path, password: str = "") -> AasHttpClient | None:
|
|
256
|
+
"""Create a BaSyx server interface client from the given parameters.
|
|
257
|
+
|
|
258
|
+
:param config_file: Path to the configuration file containing the BaSyx server connection settings.
|
|
259
|
+
:param password: password for the BaSyx server interface client, defaults to ""_
|
|
260
|
+
:return: An instance of HttpClient initialized with the provided parameters.
|
|
261
|
+
"""
|
|
262
|
+
logger.info(f"Create BaSyx server interface client from config file '{config_file}'")
|
|
263
|
+
if not config_file.exists():
|
|
264
|
+
config_string = "{}"
|
|
265
|
+
logger.warning(f"Server config file '{config_file}' not found. Using default config.")
|
|
266
|
+
else:
|
|
267
|
+
config_string = config_file.read_text(encoding="utf-8")
|
|
268
|
+
logger.debug(f"Server config file '{config_file}' found.")
|
|
269
|
+
|
|
270
|
+
wrapper = SdkWrapper()
|
|
271
|
+
wrapper._client = _create_client(config_string, password)
|
|
272
|
+
return wrapper
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
aas_http_client/__init__.py
|
|
5
|
+
aas_http_client/client.py
|
|
6
|
+
aas_http_client.egg-info/PKG-INFO
|
|
7
|
+
aas_http_client.egg-info/SOURCES.txt
|
|
8
|
+
aas_http_client.egg-info/dependency_links.txt
|
|
9
|
+
aas_http_client.egg-info/top_level.txt
|
|
10
|
+
aas_http_client/core/encoder.py
|
|
11
|
+
aas_http_client/core/version_check.py
|
|
12
|
+
aas_http_client/demo/demo_process.py
|
|
13
|
+
aas_http_client/demo/logging_handler.py
|
|
14
|
+
aas_http_client/utilities/__init__.py
|
|
15
|
+
aas_http_client/utilities/model_builder.py
|
|
16
|
+
aas_http_client/wrapper/python_sdk_wrapper_tmp.py
|
|
17
|
+
aas_http_client/wrapper/sdk_wrapper.py
|
|
18
|
+
tests/test_client.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "aas-http-client"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.5"
|
|
8
8
|
description = "Generic HTTP client for communicating with various types of AAS servers"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from aas_http_client.client import create_client_by_config, AasHttpClient
|
|
4
|
+
|
|
5
|
+
@pytest.fixture(scope="module")
|
|
6
|
+
def cloud_client() -> AasHttpClient:
|
|
7
|
+
try:
|
|
8
|
+
file = Path("./tests/test_server_config.json").resolve()
|
|
9
|
+
|
|
10
|
+
if not file.exists():
|
|
11
|
+
raise FileNotFoundError(f"Configuration file {file} does not exist.")
|
|
12
|
+
|
|
13
|
+
client = create_client_by_config(file, password="")
|
|
14
|
+
except Exception as e:
|
|
15
|
+
raise RuntimeError("Unable to connect to server.")
|
|
16
|
+
|
|
17
|
+
return client
|
|
18
|
+
|
|
19
|
+
def test_001_connect(cloud_client: AasHttpClient):
|
|
20
|
+
print("Testing connection to the server...")
|
|
21
|
+
assert cloud_client is not None
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
LICENSE
|
|
2
|
-
README.md
|
|
3
|
-
pyproject.toml
|
|
4
|
-
aas_http_client/__init__.py
|
|
5
|
-
aas_http_client/client.py
|
|
6
|
-
aas_http_client.egg-info/PKG-INFO
|
|
7
|
-
aas_http_client.egg-info/SOURCES.txt
|
|
8
|
-
aas_http_client.egg-info/dependency_links.txt
|
|
9
|
-
aas_http_client.egg-info/top_level.txt
|
|
10
|
-
aas_http_client/core/encoder.py
|
|
11
|
-
aas_http_client/core/version_check.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|