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.

Files changed (21) hide show
  1. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/PKG-INFO +1 -1
  2. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/__init__.py +7 -3
  3. aas_http_client-0.1.5/aas_http_client/client.py +567 -0
  4. aas_http_client-0.1.5/aas_http_client/demo/demo_process.py +74 -0
  5. aas_http_client-0.1.5/aas_http_client/demo/logging_handler.py +177 -0
  6. aas_http_client-0.1.5/aas_http_client/utilities/__init__.py +0 -0
  7. aas_http_client-0.1.5/aas_http_client/utilities/model_builder.py +114 -0
  8. aas_http_client-0.1.5/aas_http_client/wrapper/sdk_wrapper.py +272 -0
  9. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/PKG-INFO +1 -1
  10. aas_http_client-0.1.5/aas_http_client.egg-info/SOURCES.txt +18 -0
  11. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/pyproject.toml +1 -1
  12. aas_http_client-0.1.5/tests/test_client.py +21 -0
  13. aas_http_client-0.1.4/aas_http_client.egg-info/SOURCES.txt +0 -11
  14. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/LICENSE +0 -0
  15. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/README.md +0 -0
  16. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/core/encoder.py +0 -0
  17. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client/core/version_check.py +0 -0
  18. /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
  19. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/dependency_links.txt +0 -0
  20. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/aas_http_client.egg-info/top_level.txt +0 -0
  21. {aas_http_client-0.1.4 → aas_http_client-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-http-client
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Generic HTTP client for communicating with various types of AAS servers
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -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
- __version__ = importlib.metadata.version(__name__)
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, AasxServerInterface
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", "AasxServerInterface"]
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()
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aas-http-client
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Generic HTTP client for communicating with various types of AAS servers
5
5
  Author-email: Daniel Klein <daniel.klein@em.ag>
6
6
  License: MIT License
@@ -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.4"
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