aas-http-client 0.1.4__py3-none-any.whl → 0.1.5__py3-none-any.whl

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.

@@ -0,0 +1,641 @@
1
+ """BaSyx Server interface for REST API communication."""
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ import time
7
+ from pathlib import Path
8
+
9
+ import basyx.aas.adapter.json
10
+ import basyx.aas.adapter.json.json_serialization as js
11
+ import requests
12
+ from basyx.aas.model import AssetAdministrationShell, Reference, Submodel
13
+ from aas_http_client.core.encoder import decode_base_64, encode_base_64
14
+ from pydantic import BaseModel, PrivateAttr, ValidationError
15
+ from requests import Session
16
+ from requests.auth import HTTPBasicAuth
17
+ from requests.models import Response
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ STATUS_CODE_200 = 200
22
+ STATUS_CODE_201 = 201
23
+ STATUS_CODE_202 = 202
24
+ STATUS_CODE_204 = 204
25
+ HEADERS = {"Content-Type": "application/json"}
26
+
27
+
28
+ def log_response_errors(response: Response):
29
+ """Create error messages from the response and log them.
30
+
31
+ :param response: response
32
+ """
33
+ result_error_messages: list[str] = []
34
+
35
+ try:
36
+ response_content_dict: dict = json.loads(response.content)
37
+
38
+ if "detail" in response_content_dict:
39
+ detail: dict = response_content_dict.get("detail", {})
40
+ if "error" in detail:
41
+ error: str = detail.get("error", "")
42
+ result_error_messages.append(f"{error}")
43
+ else:
44
+ result_error_messages.append(f"{detail}")
45
+
46
+ elif "messages" in response_content_dict or "Messages" in response_content_dict:
47
+ messages: list = response_content_dict.get("messages", [])
48
+
49
+ if not messages:
50
+ messages = response_content_dict.get("Messages", [])
51
+
52
+ for message in messages:
53
+ if isinstance(message, dict) and "message" in message:
54
+ result_error_messages.append(message["message"])
55
+ else:
56
+ result_error_messages.append(str(message))
57
+ elif "error" in response_content_dict:
58
+ result_error_messages.append(response_content_dict.get("error", ""))
59
+
60
+ except json.JSONDecodeError:
61
+ result_error_messages.append(response.content)
62
+
63
+ logger.error(f"Status code: {response.status_code}")
64
+ for result_error_message in result_error_messages:
65
+ logger.error(result_error_message)
66
+
67
+
68
+ class AasxServerInterface(BaseModel):
69
+ """Represents a AasxServerInterface to communicate with a REST API."""
70
+
71
+ base_url: str = "http://basyx_aas_server:80/"
72
+ username: str | None = None
73
+ _password: str | None = PrivateAttr(default=None)
74
+ https_proxy: str | None = None
75
+ http_proxy: str | None = None
76
+ time_out: int = 200
77
+ connection_time_out: int = 100
78
+ ssl_verify: bool = True
79
+ _session: Session = PrivateAttr(default=None)
80
+ namespace: str | None = None
81
+
82
+ def initialize(self, password: str):
83
+ """Initialize the AasxServerInterface with the given URL, username and password.
84
+
85
+ :param password: password
86
+ """
87
+ self._password = password
88
+
89
+ if self.base_url.endswith("/"):
90
+ self.base_url = self.base_url[:-1]
91
+
92
+ self._session = requests.Session()
93
+ self._session.auth = HTTPBasicAuth(self.username, self._password)
94
+ self._session.verify = self.ssl_verify
95
+
96
+ if self.https_proxy:
97
+ self._session.proxies.update({"https": self.https_proxy})
98
+ if self.http_proxy:
99
+ self._session.proxies.update({"http": self.http_proxy})
100
+
101
+ if not self.namespace:
102
+ self.namespace = re.sub(r":\d+", "", self.base_url)
103
+
104
+ def _get_namespace(self) -> str:
105
+ """Get the address of the REST API.
106
+
107
+ :return: address as a string
108
+ """
109
+ match = re.search(r"^https?://([^:/]+)", self.base_url)
110
+
111
+ if match:
112
+ return match.group(1)
113
+
114
+ raise ValueError(f"Invalid URL format: {self.base_url}")
115
+
116
+ def get_root(self) -> dict | None:
117
+ """Get the root of the REST API.
118
+
119
+ :return: root data as a dictionary or None if an error occurred
120
+ """
121
+ url = f"{self.base_url}/shells"
122
+
123
+ try:
124
+ response = self._session.get(url, headers=HEADERS, timeout=2)
125
+ logger.debug(f"Call REST API url '{response.url}'")
126
+
127
+ if response.status_code != STATUS_CODE_200:
128
+ log_response_errors(response)
129
+ return None
130
+
131
+ except requests.exceptions.RequestException as e:
132
+ logger.error(f"Error call REST API: {e}")
133
+ return None
134
+
135
+ return json.loads(response.content)
136
+
137
+ def post_shells(self, aas: AssetAdministrationShell) -> dict | None:
138
+ """Post an Asset Administration Shell (AAS) to the REST API.
139
+
140
+ :param aas: Asset Administration Shell to post
141
+ :return: Response data as a dictionary or None if an error occurred
142
+ """
143
+ aas_dict_string = json.dumps(aas, cls=basyx.aas.adapter.json.AASToJsonEncoder)
144
+ aas_dict = json.loads(aas_dict_string)
145
+
146
+ url = f"{self.base_url}/shells"
147
+ logger.debug(f"Call REST API url '{url}'")
148
+
149
+ try:
150
+ response = self._session.post(url, headers=HEADERS, json=aas_dict, timeout=self.time_out)
151
+ logger.debug(f"Call REST API url '{response.url}'")
152
+
153
+ if response.status_code not in (STATUS_CODE_201, STATUS_CODE_202):
154
+ log_response_errors(response)
155
+ return None
156
+
157
+ except requests.exceptions.RequestException as e:
158
+ logger.error(f"Error call REST API: {e}")
159
+ return None
160
+
161
+ content = json.loads(response.content)
162
+ return dict(sorted(content.items()))
163
+
164
+ def put_shells(self, identifier: str, aas: AssetAdministrationShell) -> bool:
165
+ """Update an Asset Administration Shell (AAS) by its ID in the REST API.
166
+
167
+ :param identifier: Identifier of the AAS to update
168
+ :param aas: Asset Administration Shell data to update
169
+ :return: True if the update was successful, False otherwise
170
+ """
171
+ aas_dict_string = json.dumps(aas, cls=basyx.aas.adapter.json.AASToJsonEncoder)
172
+ aas_dict = json.loads(aas_dict_string)
173
+
174
+ decoded_identifier: str = decode_base_64(identifier)
175
+ url = f"{self.base_url}/shells/{decoded_identifier}"
176
+
177
+ try:
178
+ response = self._session.put(url, headers=HEADERS, json=aas_dict, timeout=self.time_out)
179
+ logger.debug(f"Call REST API url '{response.url}'")
180
+
181
+ if response.status_code is not STATUS_CODE_204:
182
+ log_response_errors(response)
183
+ return False
184
+
185
+ except requests.exceptions.RequestException as e:
186
+ logger.error(f"Error call REST API: {e}")
187
+ return False
188
+
189
+ return True
190
+
191
+ def put_shells_submodels(self, aas_id: str, submodel_id: str, submodel: Submodel) -> bool:
192
+ """Update a submodel by its ID for a specific Asset Administration Shell (AAS).
193
+
194
+ :param aas_id: ID of the AAS to update the submodel for
195
+ :param submodel: Submodel data to update
196
+ :return: True if the update was successful, False otherwise
197
+ """
198
+ sm_dict_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
199
+ sm_dict = json.loads(sm_dict_string)
200
+
201
+ decoded_aas_id: str = decode_base_64(aas_id)
202
+ decoded_submodel_id: str = decode_base_64(submodel_id)
203
+ url = f"{self.base_url}/shells/{decoded_aas_id}/submodels/{decoded_submodel_id}"
204
+
205
+ try:
206
+ response = self._session.put(url, headers=HEADERS, json=sm_dict, timeout=self.time_out)
207
+ logger.debug(f"Call REST API url '{response.url}'")
208
+
209
+ if response.status_code != STATUS_CODE_204:
210
+ log_response_errors(response)
211
+ return False
212
+
213
+ except requests.exceptions.RequestException as e:
214
+ logger.error(f"Error call REST API: {e}")
215
+ return False
216
+
217
+ return True
218
+
219
+ def get_shells(self) -> list[AssetAdministrationShell] | None:
220
+ """Get all Asset Administration Shells (AAS) from the REST API.
221
+
222
+ :return: AAS objects or None if an error occurred
223
+ """
224
+ url = f"{self.base_url}/shells"
225
+
226
+ try:
227
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
228
+ logger.debug(f"Call REST API url '{response.url}'")
229
+
230
+ if response.status_code != STATUS_CODE_200:
231
+ log_response_errors(response)
232
+ return None
233
+
234
+ except requests.exceptions.RequestException as e:
235
+ logger.error(f"Error call REST API: {e}")
236
+ return None
237
+
238
+ content: list = json.loads(response.content)
239
+
240
+ if not content:
241
+ logger.warning("No AAS found in the REST API.")
242
+ return []
243
+
244
+ results: list = content.get("result", [])
245
+ if not results:
246
+ logger.warning("No AAS found in the REST API results.")
247
+ return []
248
+
249
+ aas_list: list[AssetAdministrationShell] = []
250
+
251
+ for result in results:
252
+ if not isinstance(result, dict):
253
+ logger.error(f"Invalid AAS data: {result}")
254
+ return None
255
+
256
+ aas_dict_string = json.dumps(result)
257
+ aas = json.loads(aas_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
258
+ aas_list.append(aas)
259
+
260
+ return aas_list
261
+
262
+ def get_shells_by_id(self, aas_id: str) -> AssetAdministrationShell | None:
263
+ """Get an Asset Administration Shell (AAS) by its ID from the REST API.
264
+
265
+ :param aas_id: ID of the AAS to retrieve
266
+ :return: AAS object or None if an error occurred
267
+ """
268
+ decoded_aas_id: str = decode_base_64(aas_id)
269
+ url = f"{self.base_url}/shells/{decoded_aas_id}"
270
+
271
+ try:
272
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
273
+ logger.debug(f"Call REST API url '{response.url}'")
274
+
275
+ if response.status_code != STATUS_CODE_200:
276
+ log_response_errors(response)
277
+ return None
278
+
279
+ except requests.exceptions.RequestException as e:
280
+ logger.error(f"Error call REST API: {e}")
281
+ return None
282
+
283
+ aas_dict_string = response.content.decode("utf-8")
284
+ return json.loads(aas_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
285
+
286
+ def get_shells_reference_by_id(self, aas_id: str) -> Reference | None:
287
+ decoded_aas_id: str = decode_base_64(aas_id)
288
+ url = f"{self.base_url}/shells/{decoded_aas_id}/$reference"
289
+
290
+ try:
291
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
292
+ logger.debug(f"Call REST API url '{response.url}'")
293
+
294
+ if response.status_code != STATUS_CODE_200:
295
+ log_response_errors(response)
296
+ return None
297
+
298
+ except requests.exceptions.RequestException as e:
299
+ logger.error(f"Error call REST API: {e}")
300
+ return None
301
+
302
+ ref_dict_string = response.content.decode("utf-8")
303
+ return json.loads(ref_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
304
+
305
+ def get_shells_submodels(self, aas_id: str, submodel_id: str) -> Submodel | None:
306
+ """Get a submodel by its ID for a specific Asset Administration Shell (AAS).
307
+
308
+ :param aas_id: ID of the AAS to retrieve the submodel from
309
+ :param submodel_id: ID of the submodel to retrieve
310
+ :return: Submodel object or None if an error occurred
311
+ """
312
+ decoded_aas_id: str = decode_base_64(aas_id)
313
+ decoded_submodel_id: str = decode_base_64(submodel_id)
314
+
315
+ url = f"{self.base_url}/shells/{decoded_aas_id}/submodels/{decoded_submodel_id}"
316
+
317
+ try:
318
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
319
+ logger.debug(f"Call REST API url '{response.url}'")
320
+
321
+ if response.status_code != STATUS_CODE_200:
322
+ log_response_errors(response)
323
+ return None
324
+
325
+ except requests.exceptions.RequestException as e:
326
+ logger.error(f"Error call REST API: {e}")
327
+ return None
328
+
329
+ submodel_dict_string = response.content.decode("utf-8")
330
+ return json.loads(submodel_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
331
+
332
+ def delete_shells_by_id(self, aas_id: str) -> bool:
333
+ """Get an Asset Administration Shell (AAS) by its ID from the REST API.
334
+
335
+ :param aas_id: ID of the AAS to retrieve
336
+ :return: True if the deletion was successful, False otherwise
337
+ """
338
+ decoded_aas_id: str = decode_base_64(aas_id)
339
+ url = f"{self.base_url}/shells/{decoded_aas_id}"
340
+
341
+ try:
342
+ response = self._session.delete(url, headers=HEADERS, timeout=self.time_out)
343
+ logger.debug(f"Call REST API url '{response.url}'")
344
+
345
+ if response.status_code != STATUS_CODE_204:
346
+ log_response_errors(response)
347
+ return False
348
+
349
+ except requests.exceptions.RequestException as e:
350
+ logger.error(f"Error call REST API: {e}")
351
+ return False
352
+
353
+ return True
354
+
355
+ def post_submodels(self, submodel: Submodel) -> bool:
356
+ """Post a submodel to the REST API.
357
+
358
+ :param submodel: submodel data as a dictionary
359
+ :return: Response data as a dictionary or None if an error occurred
360
+ """
361
+ sm_dict_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
362
+ sm_dict = json.loads(sm_dict_string)
363
+
364
+ url = f"{self.base_url}/submodels"
365
+
366
+ try:
367
+ response = self._session.post(url, headers=HEADERS, json=sm_dict, timeout=self.time_out)
368
+ logger.debug(f"Call REST API url '{response.url}'")
369
+
370
+ if response.status_code not in (STATUS_CODE_201, STATUS_CODE_202):
371
+ log_response_errors(response)
372
+ return False
373
+
374
+ except requests.exceptions.RequestException as e:
375
+ logger.error(f"Error call REST API: {e}")
376
+ return False
377
+
378
+ return True
379
+
380
+ def put_submodels(self, identifier: str, submodel: Submodel) -> bool:
381
+ """Update a submodel by its ID in the REST API.
382
+
383
+ :param identifier: Identifier of the submodel to update
384
+ :param submodel: Submodel data to update
385
+ :return: True if the update was successful, False otherwise
386
+ """
387
+ sm_dict_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
388
+ sm_dict = json.loads(sm_dict_string)
389
+
390
+ decoded_identifier: str = decode_base_64(identifier)
391
+ url = f"{self.base_url}/submodels/{decoded_identifier}"
392
+
393
+ try:
394
+ response = self._session.put(url, headers=HEADERS, json=sm_dict, timeout=self.time_out)
395
+ logger.debug(f"Call REST API url '{response.url}'")
396
+
397
+ if response.status_code != STATUS_CODE_204:
398
+ log_response_errors(response)
399
+ return False
400
+
401
+ except requests.exceptions.RequestException as e:
402
+ logger.error(f"Error call REST API: {e}")
403
+ return False
404
+
405
+ return True
406
+
407
+ def get_submodel_by_id(self, submodel_id: str) -> Submodel | None:
408
+ """Get a submodel by its ID from the REST API.
409
+
410
+ :param submodel_id: ID of the submodel to retrieve
411
+ :return: Submodel object or None if an error occurred
412
+ """
413
+ decoded_submodel_id: str = decode_base_64(submodel_id)
414
+ url = f"{self.base_url}/submodels/{decoded_submodel_id}"
415
+
416
+ try:
417
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
418
+ logger.debug(f"Call REST API url '{response.url}'")
419
+
420
+ if response.status_code != STATUS_CODE_200:
421
+ log_response_errors(response)
422
+ return None
423
+
424
+ except requests.exceptions.RequestException as e:
425
+ logger.error(f"Error call REST API: {e}")
426
+ return None
427
+
428
+ sm_dict_string = response.content.decode("utf-8")
429
+ return json.loads(sm_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
430
+
431
+ def get_submodels(self) -> list[Submodel] | None:
432
+ """Get all submodels from the REST API.
433
+
434
+ :return: Submodel objects or None if an error occurred
435
+ """
436
+ url = f"{self.base_url}/submodels"
437
+
438
+ try:
439
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
440
+ logger.debug(f"Call REST API url '{response.url}'")
441
+
442
+ if response.status_code != STATUS_CODE_200:
443
+ log_response_errors(response)
444
+ return None
445
+
446
+ except requests.exceptions.RequestException as e:
447
+ logger.error(f"Error call REST API: {e}")
448
+ return None
449
+
450
+ content: list = json.loads(response.content)
451
+
452
+ if not content:
453
+ logger.warning("No submodels found in the REST API.")
454
+ return []
455
+
456
+ results: list = content.get("result", [])
457
+ if not results:
458
+ logger.warning("No submodels found in the REST API results.")
459
+ return []
460
+
461
+ submodels: list[Submodel] = []
462
+
463
+ for result in results:
464
+ if not isinstance(result, dict):
465
+ logger.error(f"Invalid submodel data: {result}")
466
+ return None
467
+
468
+ sm_dict_string = json.dumps(result)
469
+ submodel = json.loads(sm_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
470
+ submodels.append(submodel)
471
+
472
+ return submodels
473
+
474
+ def get_submodels_by_id(self, submodel_id: str) -> Submodel | None:
475
+ """Get a submodel by its ID from the REST API.
476
+
477
+ :param submodel_id: ID of the submodel to retrieve
478
+ :return: Submodel object or None if an error occurred
479
+ """
480
+ decoded_submodel_id: str = decode_base_64(submodel_id)
481
+ url = f"{self.base_url}/submodels/{decoded_submodel_id}"
482
+
483
+ try:
484
+ response = self._session.get(url, headers=HEADERS, timeout=self.time_out)
485
+ logger.debug(f"Call REST API url '{response.url}'")
486
+
487
+ if response.status_code != STATUS_CODE_200:
488
+ log_response_errors(response)
489
+ return None
490
+
491
+ except requests.exceptions.RequestException as e:
492
+ logger.error(f"Error call REST API: {e}")
493
+ return None
494
+
495
+ sm_dict_string = response.content.decode("utf-8")
496
+ return json.loads(sm_dict_string, cls=basyx.aas.adapter.json.AASFromJsonDecoder)
497
+
498
+ def patch_submodel_by_id(self, submodel_id: str, submodel: Submodel):
499
+ sm_dict_string = json.dumps(submodel, cls=basyx.aas.adapter.json.AASToJsonEncoder)
500
+ sm_dict = json.loads(sm_dict_string)
501
+
502
+ decoded_submodel_id: str = decode_base_64(submodel_id)
503
+ url = f"{self.base_url}/submodels/{decoded_submodel_id}"
504
+
505
+ try:
506
+ response = self._session.patch(url, headers=HEADERS, json=sm_dict, timeout=self.time_out)
507
+ logger.debug(f"Call REST API url '{response.url}'")
508
+
509
+ if response.status_code != STATUS_CODE_204:
510
+ log_response_errors(response)
511
+ return False
512
+
513
+ except requests.exceptions.RequestException as e:
514
+ logger.error(f"Error call REST API: {e}")
515
+ return False
516
+
517
+ return True
518
+
519
+ def delete_submodels_by_id(self, submodel_id: str) -> bool:
520
+ """Delete a submodel by its ID from the REST API.
521
+
522
+ :param submodel_id: ID of the submodel to delete
523
+ :return: True if the deletion was successful, False otherwise
524
+ """
525
+ decoded_submodel_id: str = decode_base_64(submodel_id)
526
+ url = f"{self.base_url}/submodels/{decoded_submodel_id}"
527
+
528
+ try:
529
+ response = self._session.delete(url, headers=HEADERS, timeout=self.time_out)
530
+ logger.debug(f"Call REST API url '{response.url}'")
531
+
532
+ if response.status_code != STATUS_CODE_204:
533
+ log_response_errors(response)
534
+ return False
535
+
536
+ except requests.exceptions.RequestException as e:
537
+ logger.error(f"Error call REST API: {e}")
538
+ return False
539
+
540
+ return True
541
+
542
+
543
+ def create_client_by_url(
544
+ base_url: str,
545
+ username: str = "",
546
+ password: str = "",
547
+ http_proxy: str = "",
548
+ https_proxy: str = "",
549
+ time_out: int = 200,
550
+ connection_time_out: int = 60,
551
+ ssl_verify: str = True, # noqa: FBT002
552
+ namespace: str = "",
553
+ ) -> AasxServerInterface | None:
554
+ """Create a BaSyx server interface client from the given parameters.
555
+
556
+ :param base_url: base URL of the BaSyx server, e.g. "http://basyx_python_server:80/"_
557
+ :param username: username for the BaSyx server interface client, defaults to ""_
558
+ :param password: password for the BaSyx server interface client, defaults to ""_
559
+ :param http_proxy: http proxy URL, defaults to ""_
560
+ :param https_proxy: https proxy URL, defaults to ""_
561
+ :param time_out: timeout for the API calls, defaults to 200
562
+ :param connection_time_out: timeout for the connection to the API, defaults to 60
563
+ :param ssl_verify: whether to verify SSL certificates, defaults to True
564
+ :return: An instance of AasxServerInterface initialized with the provided parameters.
565
+ """
566
+ logger.info(f"Create BaSyx server interface client from URL '{base_url}'")
567
+ config_dict: dict[str, str] = {}
568
+ config_dict["base_url"] = base_url
569
+ config_dict["username"] = username
570
+ config_dict["http_proxy"] = http_proxy
571
+ config_dict["https_proxy"] = https_proxy
572
+ config_dict["time_out"] = time_out
573
+ config_dict["connection_time_out"] = connection_time_out
574
+ config_dict["ssl_verify"] = ssl_verify
575
+ config_dict["namespace"] = namespace
576
+ config_string = json.dumps(config_dict, indent=4)
577
+ return _create_client(config_string, password)
578
+
579
+
580
+ def create_client_by_config(config_file: Path, password: str = "") -> AasxServerInterface | None:
581
+ """Create a BaSyx server interface client from the given parameters.
582
+
583
+ :param config_file: Path to the configuration file containing the BaSyx server connection settings.
584
+ :param password: password for the BaSyx server interface client, defaults to ""_
585
+ :return: An instance of AasxServerInterface initialized with the provided parameters.
586
+ """
587
+ logger.info(f"Create BaSyx server interface client from config file '{config_file}'")
588
+ if not config_file.exists():
589
+ config_string = "{}"
590
+ logger.warning(f"Server config file '{config_file}' not found. Using default config.")
591
+ else:
592
+ config_string = config_file.read_text(encoding="utf-8")
593
+ logger.debug(f"Server config file '{config_file}' found.")
594
+
595
+ return _create_client(config_string, password)
596
+
597
+
598
+ def _create_client(config_string: str, password) -> AasxServerInterface | None:
599
+ try:
600
+ connection_settings = AasxServerInterface.model_validate_json(config_string)
601
+ client = AasxServerInterface(**connection_settings.model_dump())
602
+ except ValidationError as ve:
603
+ raise ValidationError(f"Invalid BaSyx server connection file: {ve}") from ve
604
+
605
+ logger.info(
606
+ f"Using server configuration: '{client.base_url}' | "
607
+ f"timeout: '{client.time_out}' | "
608
+ f"username: '{client.username}' | "
609
+ f"https_proxy: '{client.https_proxy}' | "
610
+ f"http_proxy: '{client.http_proxy}' | "
611
+ f"connection_timeout: '{client.connection_time_out}'"
612
+ )
613
+ client.initialize(password)
614
+
615
+ # test the connection to the REST API
616
+ connected = _connect_to_api(client)
617
+
618
+ if not connected:
619
+ return None
620
+
621
+ return client
622
+
623
+
624
+ def _connect_to_api(client: AasxServerInterface) -> bool:
625
+ start_time = time.time()
626
+ logger.debug(f"Try to connect to REST API '{client.base_url}' for {client.connection_time_out} seconds")
627
+ counter: int = 0
628
+ while True:
629
+ try:
630
+ root = client.get_root()
631
+ if root:
632
+ logger.info(f"Connected to REST API at '{client.base_url}' successfully.")
633
+ return True
634
+ except requests.exceptions.ConnectionError:
635
+ pass
636
+ if time.time() - start_time > client.connection_time_out:
637
+ raise TimeoutError(f"Connection to REST API timed out after {client.connection_time_out} seconds.")
638
+
639
+ counter += 1
640
+ logger.warning(f"Retrying connection (attempt: {counter})")
641
+ time.sleep(1)