AAS HTTP Client Documentation
Loading...
Searching...
No Matches
aas_client.py
Go to the documentation of this file.
1"""Client for HTTP API communication with AAS server."""
2
3import json
4import logging
5import time
6from pathlib import Path
7from typing import Any
8
9import requests
10from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError
11from requests import Session
12from requests.auth import HTTPBasicAuth
13
14from aas_http_client.classes.client.implementations import (
15 AuthMethod,
16 ExperimentalImplementation,
17 ShellRegistryImplementation,
18 ShellRepoImplementation,
19 SubmodelRegistryImplementation,
20 SubmodelRepoImplementation,
21 TokenData,
22 get_token,
23)
24from aas_http_client.classes.Configuration.config_classes import AuthenticationConfig
25from aas_http_client.utilities.constants import LogIntensity
27 STATUS_CODE_200,
28 STATUS_CODE_201,
29 STATUS_CODE_202,
30 STATUS_CODE_204,
31)
32
33_logger = logging.getLogger(__name__)
34
35
36class AASConnectionError(ConnectionError):
37 """Exception raised for errors in the AAS connection.
38
39 :param message: Error message
40 :param errors: Dictionary of error codes and their corresponding messages
41 """
42
43 def __init__(self, message: str, errors: dict[int, str]):
44 """
45 Initialize the AASConnectionError.
46
47 :param message: Error message
48 :param errors: Dictionary of error codes and their corresponding messages
49 """
50 super().__init__(message)
51 self.errors = errors
52 self.status_code = next(iter(errors), None) # Get the first status code if available
53
55class AasHttpClient(BaseModel):
56 """Represents a AasHttpClient to communicate with a REST API."""
57
58 model_config = ConfigDict(populate_by_name=True, arbitrary_types_allowed=True)
60 base_url: str = Field(..., alias="BaseUrl", description="Base URL of the AAS server.")
61 auth_settings: AuthenticationConfig = Field(
62 default_factory=AuthenticationConfig, alias="AuthenticationSettings", description="Authentication settings for the AAS server."
63 )
64 https_proxy: str | None = Field(default=None, alias="HttpsProxy", description="HTTPS proxy URL.")
65 http_proxy: str | None = Field(default=None, alias="HttpProxy", description="HTTP proxy URL.")
66 time_out: int = Field(default=200, alias="TimeOut", description="Timeout for HTTP requests.")
67 connection_time_out: int = Field(default=100, alias="ConnectionTimeOut", description="Connection timeout for HTTP requests.")
68 ssl_verify: bool = Field(default=True, alias="SslVerify", description="Enable SSL verification.")
69 trust_env: bool = Field(default=True, alias="TrustEnv", description="Trust environment variables.")
70 _session: Session | None = PrivateAttr(default=None)
71 _auth_method: AuthMethod = PrivateAttr(default=AuthMethod.basic_auth)
72 encoded_ids: bool = Field(default=True, alias="EncodedIds", description="If enabled, all IDs used in API requests have to be base64-encoded.")
73 shells: ShellRepoImplementation | None = Field(default=None)
74 submodels: SubmodelRepoImplementation | None = Field(default=None)
75 shell_registry: ShellRegistryImplementation | None = Field(default=None)
76 experimental: ExperimentalImplementation | None = Field(default=None)
77 submodel_registry: SubmodelRegistryImplementation | None = Field(default=None)
78 _cached_token: TokenData | None = PrivateAttr(default=None)
79 _log_intensity: LogIntensity = PrivateAttr(default=LogIntensity.HIGH)
81 def initialize(self):
82 """Initialize the AasHttpClient with the given URL, username and password."""
83 if self.base_urlbase_url.endswith("/"):
84 self.base_urlbase_url = self.base_urlbase_url[:-1]
86 self._session_session = requests.Session()
90 self._session_session.verify = self.ssl_verify
91 self._session_session.trust_env = self.trust_env
92
93 if self.https_proxy:
94 self._session_session.proxies.update({"https": self.https_proxy})
95 if self.http_proxy:
96 self._session_session.proxies.update({"http": self.http_proxy})
97
98 self._session_session.headers.update(
99 {
100 "Accept": "*/*",
101 "User-Agent": "python-requests/2.32.5",
102 "Connection": "close",
103 }
104 )
105
106 self.shellsshells = ShellRepoImplementation(self)
107 self.submodelssubmodels = SubmodelRepoImplementation(self)
108 self.shell_registryshell_registry = ShellRegistryImplementation(self)
109 self.submodel_registrysubmodel_registry = SubmodelRegistryImplementation(self)
110 self.experimentalexperimental = ExperimentalImplementation(self)
112 def set_log_intensity(self, intensity: LogIntensity):
113 """Set the log intensity level for the client.
114
115 :param intensity: LogIntensity level to set (Standard or High)
116 """
117 self._log_intensity_log_intensity = intensity
118
119 def get_log_intensity(self) -> LogIntensity:
120 """Get the log intensity level for the client.
121
122 :return: The current LogIntensity level (Standard or High)
123 """
125
126 def get_auth_method(self) -> AuthMethod:
127 """Get the authentication method used by the client.
128
129 :return: The authentication method used by the client
130 """
131 return self._auth_method_auth_method
132
133 def get_session(self) -> Session | None:
134 """Get the HTTP session used by the client.
135
136 :return: The requests.Session object used for HTTP communication
137 """
138 return self._session_session
139
141 """Handles the authentication method based on the provided settings."""
142 if self.auth_settings.o_auth.is_active():
143 self._auth_method_auth_method = AuthMethod.o_auth
144 _logger.debug(
145 f"Authentication method: OAuth | '{self.auth_settings.o_auth.client_id}' | '{self.auth_settings.o_auth.token_url}' | '{self.auth_settings.o_auth.grant_type}'"
147
148 elif self.auth_settings.basic_auth.is_active():
149 self._auth_method_auth_method = AuthMethod.basic_auth
150 _logger.debug(f"Authentication method: Basic Auth | '{self.auth_settings.basic_auth.username}'")
151 self._session_session.auth = HTTPBasicAuth(self.auth_settings.basic_auth.username, self.auth_settings.basic_auth.get_password())
152
153 elif self.auth_settings.bearer_auth.is_active():
154 self._auth_method_auth_method = AuthMethod.bearer
155 _logger.debug("Authentication method: Bearer Token")
156 self._session_session.headers.update({"Authorization": f"Bearer {self.auth_settings.bearer_auth.get_token()}"})
157
158 else:
159 self._auth_method_auth_method = AuthMethod.No
160 _logger.debug("Authentication method: No Authentication")
161
162 def get_root(self) -> dict | None:
163 """Get the root endpoint of the AAS server API to test connectivity.
164
165 This method calls the '/shells' endpoint to verify that the AAS server is accessible
166 and responding. It automatically handles authentication token setup if service
167 provider authentication is configured.
168
169 :return: Response data as a dictionary containing shell information, or None if an error occurred
170 """
171 if not self._session_session:
172 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
173 return None
174
175 urls: list[str] = []
176 urls.append(f"{self.base_url}/shells")
177 urls.append(f"{self.base_url}/submodels")
178 urls.append(f"{self.base_url}/shell-descriptors")
179 urls.append(f"{self.base_url}/submodel-descriptors")
180
181 self.set_token()
182
183 error_messages: dict[int, str] = {}
184
185 for url in urls:
186 _logger.debug(f"Testing connectivity with URL: {url}")
187 try:
188 response = self._session_session.get(url, timeout=10)
189 _logger.debug(f"Call REST API url '{response.url}'")
190
191 if response.status_code == STATUS_CODE_200:
192 content = response.content.decode("utf-8")
193 return json.loads(content)
194
195 if response.status_code not in (STATUS_CODE_200, STATUS_CODE_201, STATUS_CODE_204):
196 error_messages.update({response.status_code: response.reason})
197
198 except requests.exceptions.RequestException as e:
199 _logger.error(f"Error call REST API: {e}")
200
201 raise AASConnectionError("Failed to connect to AAS server API", error_messages)
202
203 def set_token(self) -> str | None:
204 """Set authentication token in session headers based on configured authentication method.
205
206 :return: The access token if set, otherwise None
207 """
208 if not self._session_session:
209 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
210 return None
211
212 if self._auth_method_auth_method != AuthMethod.o_auth:
213 return None
214
215 now = time.time()
216 # Check if cached token exists and is not expired
217 if self._cached_token_cached_token and self._cached_token_cached_token.token_expiry > now:
218 return self._cached_token_cached_token.access_token
219
220 # Obtain new token
221 token_data = get_token(self.auth_settings.o_auth, self.ssl_verify)
222
223 if token_data and token_data.access_token:
224 # Cache the token data
225 self._cached_token_cached_token = token_data
226 # Update session headers with the new token
227 self._session_session.headers.update({"Authorization": f"Bearer {self._cached_token.access_token}"})
228 return self._cached_token_cached_token.access_token
229
230 return None
231
232 def get_endpoint(self, end_point_url: str) -> None | dict:
233 """Generic GET request for endpoint.
234
235 :param end_point_url: The endpoint URL to send the GET request to.
236 :return: The base URL of the AAS server.
237 """
238 if not self._session_session:
239 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
240 return None
241
242 try:
243 response = self._session_session.get(end_point_url, timeout=self.time_out)
244 _logger.debug(f"Call REST API url '{response.url}'")
245
246 if response.status_code == STATUS_CODE_200:
247 content = response.content.decode("utf-8")
248 return json.loads(content)
249
250 except requests.exceptions.RequestException as e:
251 _logger.debug(f"Error call REST API: {e}")
252
253 return None
254
255 def put_endpoint(self, end_point_url: str, request_body: dict) -> None | dict:
256 """Generic PUT request for endpoint.
257
258 :param end_point_url: The endpoint URL to send the PUT request to.
259 :param request_body: The request body to send with the PUT request.
260 :return: The base URL of the AAS server.
261 """
262 if not self._session_session:
263 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
264 return None
265
266 try:
267 response = self._session_session.put(end_point_url, json=request_body, timeout=self.time_out)
268 _logger.debug(f"Call REST API url '{response.url}'")
269
270 if response.status_code not in (STATUS_CODE_200, STATUS_CODE_201, STATUS_CODE_204):
271 content = response.content.decode("utf-8")
272 return json.loads(content)
273
274 except requests.exceptions.RequestException as e:
275 _logger.debug(f"Error call REST API: {e}")
276
277 return None
278
279 def post_endpoint(self, end_point_url: str, request_body: dict) -> None | dict:
280 """Generic POST request for endpoint.
281
282 :param end_point_url: The endpoint URL to send the POST request to.
283 :param request_body: The request body to send with the POST request.
284 :return: The base URL of the AAS server.
285 """
286 if not self._session_session:
287 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
288 return None
289
290 try:
291 response = self._session_session.post(end_point_url, json=request_body, timeout=self.time_out)
292 _logger.debug(f"Call REST API url '{response.url}'")
293
294 if response.status_code not in (STATUS_CODE_201, STATUS_CODE_200, STATUS_CODE_202):
295 content = response.content.decode("utf-8")
296 return json.loads(content)
297
298 except requests.exceptions.RequestException as e:
299 _logger.debug(f"Error call REST API: {e}")
300
301 return None
302
303 def patch_endpoint(self, end_point_url: str, request_body: dict) -> None | dict:
304 """Generic PATCH request for endpoint.
305
306 :param end_point_url: The endpoint URL to send the PATCH request to.
307 :param request_body: The request body to send with the PATCH request.
308 :return: The base URL of the AAS server.
309 """
310 if not self._session_session:
311 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
312 return None
313
314 try:
315 response = self._session_session.patch(end_point_url, json=request_body, timeout=self.time_out)
316 _logger.debug(f"Call REST API url '{response.url}'")
317
318 if response.status_code not in (STATUS_CODE_200, STATUS_CODE_204):
319 content = response.content.decode("utf-8")
320 return json.loads(content)
321
322 except requests.exceptions.RequestException as e:
323 _logger.debug(f"Error call REST API: {e}")
324
325 return None
326
327 def delete_endpoint(self, end_point_url: str) -> None | dict:
328 """Generic DELETE request for endpoint.
329
330 :param end_point_url: The endpoint URL to send the DELETE request to.
331 :return: The base URL of the AAS server.
332 """
333 if not self._session_session:
334 _logger.error("HTTP session is not initialized. Call 'initialize()' method before making API calls.")
335 return None
336
337 try:
338 response = self._session_session.delete(end_point_url, timeout=self.time_out)
339 _logger.debug(f"Call REST API url '{response.url}'")
340
341 if response.status_code not in (STATUS_CODE_200, STATUS_CODE_204, STATUS_CODE_202):
342 content = response.content.decode("utf-8")
343 return json.loads(content)
344
345 except requests.exceptions.RequestException as e:
346 _logger.debug(f"Error call REST API: {e}")
347
348 return None
349
350
351def create_by_url( # noqa: PLR0913
352 base_url: str,
353 basic_auth_username: str = "",
354 basic_auth_password: str = "",
355 o_auth_client_id: str = "",
356 o_auth_client_secret: str = "",
357 o_auth_token_url: str = "",
358 bearer_auth_token: str = "",
359 http_proxy: str = "",
360 https_proxy: str = "",
361 time_out: int = 200,
362 connection_time_out: int = 60,
363 ssl_verify: bool = True, # noqa: FBT001, FBT002
364 trust_env: bool = True, # noqa: FBT001, FBT002
365 encoded_ids: bool = True, # noqa: FBT001, FBT002
366) -> AasHttpClient | None:
367 """Create a HTTP client for a AAS server connection from the given parameters.
368
369 :param base_url: Base URL of the AAS server, e.g. "http://basyx_python_server:80/"
370 :param basic_auth_username: Username for the AAS server basic authentication, defaults to ""
371 :param basic_auth_password: Password for the AAS server basic authentication, defaults to ""
372 :param o_auth_client_id: Client ID for OAuth authentication, defaults to ""
373 :param o_auth_client_secret: Client secret for OAuth authentication, defaults to ""
374 :param o_auth_token_url: Token URL for OAuth authentication, defaults to ""
375 :param bearer_auth_token: Bearer token for authentication, defaults to ""
376 :param http_proxy: HTTP proxy URL, defaults to ""
377 :param https_proxy: HTTPS proxy URL, defaults to ""
378 :param time_out: Timeout for the API calls, defaults to 200
379 :param connection_time_out: Timeout for the connection to the API, defaults to 60
380 :param ssl_verify: Whether to verify SSL certificates, defaults to True
381 :param trust_env: Whether to trust environment variables for proxy settings, defaults to True
382 :param encoded_ids: If enabled, all IDs used in API requests have to be base64-encoded
383 :return: An instance of AasHttpClient initialized with the provided parameters or None if connection fails
384 """
385 _logger.info(f"Create AAS server http client from URL '{base_url}'.")
386 config_dict: dict[str, Any] = {}
387 config_dict["BaseUrl"] = base_url
388 config_dict["HttpProxy"] = http_proxy
389 config_dict["HttpsProxy"] = https_proxy
390 config_dict["TimeOut"] = str(time_out)
391 config_dict["ConnectionTimeOut"] = str(connection_time_out)
392 config_dict["SslVerify"] = str(ssl_verify)
393 config_dict["TrustEnv"] = str(trust_env)
394 config_dict["EncodedIds"] = str(encoded_ids)
395
396 config_dict["AuthenticationSettings"] = {
397 "BasicAuth": {"Username": basic_auth_username},
398 "OAuth": {
399 "ClientId": o_auth_client_id,
400 "TokenUrl": o_auth_token_url,
401 },
402 }
403
404 return create_by_dict(config_dict, basic_auth_password, o_auth_client_secret, bearer_auth_token)
405
406
407def create_by_dict(
408 configuration: dict, basic_auth_password: str = "", o_auth_client_secret: str = "", bearer_auth_token: str = ""
409) -> AasHttpClient | None:
410 """Create a HTTP client for a AAS server connection from the given configuration.
411
412 :param configuration: Dictionary containing the AAS server connection settings
413 :param basic_auth_password: Password for the AAS server basic authentication, defaults to ""
414 :param o_auth_client_secret: Client secret for OAuth authentication, defaults to ""
415 :param bearer_auth_token: Bearer token for authentication, defaults to ""
416 :return: An instance of AasHttpClient initialized with the provided parameters or None if validation fails
417 """
418 _logger.info("Create AAS server http client from dictionary.")
419
420 return _create_client(configuration, basic_auth_password, o_auth_client_secret, bearer_auth_token)
421
422
423def create_by_config(
424 config_file: Path, basic_auth_password: str = "", o_auth_client_secret: str = "", bearer_auth_token: str = ""
425) -> AasHttpClient | None:
426 """Create a HTTP client for a AAS server connection from a given configuration file.
427
428 :param config_file: Path to the configuration file containing the AAS server connection settings
429 :param basic_auth_password: Password for the AAS server basic authentication, defaults to ""
430 :param o_auth_client_secret: Client secret for OAuth authentication, defaults to ""
431 :param bearer_auth_token: Bearer token for authentication, defaults to ""
432 :return: An instance of AasHttpClient initialized with the provided parameters or None if validation fails
433 """
434 config_file = config_file.resolve()
435 _logger.info(f"Create AAS server http client from configuration file '{config_file}'.")
436 if not config_file.exists():
437 configuration = {}
438 _logger.warning(f"Configuration file '{config_file}' not found. Using default configuration.")
439 else:
440 config_string = config_file.read_text(encoding="utf-8")
441 try:
442 configuration = json.loads(config_string)
443 except json.JSONDecodeError as e:
444 _logger.error(f"Configuration file '{config_file}' is not a valid JSON file: {e}")
445 return None
446 _logger.debug(f"Configuration file '{config_file}' found.")
447
448 return _create_client(configuration, basic_auth_password, o_auth_client_secret, bearer_auth_token)
449
450
451def _create_client(config_dict: dict, basic_auth_password: str, o_auth_client_secret: str, bearer_auth_token: str) -> AasHttpClient | None:
452 """Create and initialize an AAS HTTP client from configuration dictionary.
453
454 This internal method validates the configuration, sets authentication credentials,
455 initializes the client, and tests the connection to the AAS server.
456
457 :param config_dict: Dictionary containing AAS server settings
458 :param basic_auth_password: Password for basic authentication, defaults to ""
459 :param o_auth_client_secret: Client secret for OAuth authentication, defaults to ""
460 :param bearer_auth_token: Bearer token for authentication, defaults to ""
461 :return: An initialized and connected AasHttpClient instance or None if connection fails
462 :raises ValidationError: If the configuration dictionary is invalid
463 :raises TimeoutError: If connection to the server times out
464 """
465 try:
466 client = AasHttpClient.model_validate(config_dict)
467 except ValidationError as ve:
468 raise ValidationError(f"Invalid BaSyx server configuration file: {ve}") from ve
469
470 client.auth_settings.basic_auth.set_password(basic_auth_password)
471 client.auth_settings.o_auth.set_client_secret(o_auth_client_secret)
472 client.auth_settings.bearer_auth.set_token(bearer_auth_token)
473
474 _logger.debug("Using server configuration:")
475 _logger.debug(f"BaseUrl: '{client.base_url}'")
476 _logger.debug(f"TimeOut: '{client.time_out}'")
477 _logger.debug(f"HttpsProxy: '{client.https_proxy}'")
478 _logger.debug(f"HttpProxy: '{client.http_proxy}'")
479 _logger.debug(f"ConnectionTimeOut: '{client.connection_time_out}'.")
480 _logger.debug(f"SSLVerify: '{client.ssl_verify}'.")
481 _logger.debug(f"TrustEnv: '{client.trust_env}'.")
482 _logger.debug(f"EncodedIds: '{client.encoded_ids}'.")
483
484 client.initialize()
485
486 # test the connection to the REST API
487 connected = __connect_to_api(client)
488
489 if not connected:
490 return None
491
492 return client
493
494
495def __connect_to_api(client: AasHttpClient) -> bool:
496 """Test the connection to the AAS server API with retry logic.
497
498 This internal method attempts to establish a connection to the AAS server by calling
499 the get_root() method. It retries the connection for the duration specified in the
500 client's connection_time_out setting, sleeping 1 second between attempts.
501
502 :param client: The AasHttpClient instance to test the connection for
503 :return: True if connection is successful, False otherwise
504 :raises TimeoutError: If connection attempts fail for the entire timeout duration
505 """
506 start_time = time.time()
507 _logger.info(f"Try to connect to REST API '{client.base_url}' for {client.connection_time_out} seconds.")
508 counter: int = 0
509 while True:
510 try:
511 root = client.get_root()
512 if root:
513 _logger.info(f"Connected to server API at '{client.base_url}' successfully.")
514 return True
515
516 _logger.error(f"Connection attempt to '{client.base_url}' failed.")
517
518 except requests.exceptions.ConnectionError:
519 pass
520 if time.time() - start_time > client.connection_time_out:
521 raise TimeoutError(f"Connection to server API timed out after {client.connection_time_out} seconds.")
522
523 counter += 1
524 _logger.warning(f"Retrying connection (attempt: {counter}).")
525 time.sleep(1)
Exception raised for errors in the AAS connection.
Definition aas_client.py:44
__init__(self, str message, dict[int, str] errors)
Initialize the AASConnectionError.
Definition aas_client.py:52
Represents a AasHttpClient to communicate with a REST API.
Definition aas_client.py:59
LogIntensity _log_intensity
Definition aas_client.py:82
Session|None get_session(self)
Get the HTTP session used by the client.
_handle_auth_method(self)
Handles the authentication method based on the provided settings.
SubmodelRepoImplementation submodels
Definition aas_client.py:77
None|dict patch_endpoint(self, str end_point_url, dict request_body)
Generic PATCH request for endpoint.
None|dict delete_endpoint(self, str end_point_url)
Generic DELETE request for endpoint.
None|dict post_endpoint(self, str end_point_url, dict request_body)
Generic POST request for endpoint.
initialize(self)
Initialize the AasHttpClient with the given URL, username and password.
Definition aas_client.py:85
None|dict put_endpoint(self, str end_point_url, dict request_body)
Generic PUT request for endpoint.
ExperimentalImplementation experimental
Definition aas_client.py:79
set_log_intensity(self, LogIntensity intensity)
Set the log intensity level for the client.
AuthenticationConfig auth_settings
Definition aas_client.py:64
dict|None get_root(self)
Get the root endpoint of the AAS server API to test connectivity.
str|None set_token(self)
Set authentication token in session headers based on configured authentication method.
AuthMethod get_auth_method(self)
Get the authentication method used by the client.
None|dict get_endpoint(self, str end_point_url)
Generic GET request for endpoint.
SubmodelRegistryImplementation submodel_registry
Definition aas_client.py:80
ShellRepoImplementation shells
Definition aas_client.py:76
ShellRegistryImplementation shell_registry
Definition aas_client.py:78
LogIntensity get_log_intensity(self)
Get the log intensity level for the client.