1"""Client for HTTP API communication with AAS server."""
6from pathlib
import Path
10from pydantic
import BaseModel, ConfigDict, Field, PrivateAttr, ValidationError
11from requests
import Session
12from requests.auth
import HTTPBasicAuth
14from aas_http_client.classes.client.implementations
import (
16 ExperimentalImplementation,
17 ShellRegistryImplementation,
18 ShellRepoImplementation,
19 SubmodelRegistryImplementation,
20 SubmodelRepoImplementation,
24from aas_http_client.classes.Configuration.config_classes
import AuthenticationConfig
33_logger = logging.getLogger(__name__)
37 """Exception raised for errors in the AAS connection.
39 :param message: Error message
40 :param errors: Dictionary of error codes and their corresponding messages
43 def __init__(self, message: str, errors: dict[int, str]):
45 Initialize the AASConnectionError.
47 :param message: Error message
48 :param errors: Dictionary of error codes and their corresponding messages
56 """Represents a AasHttpClient to communicate with a REST API."""
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."
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)
82 """Initialize the AasHttpClient with the given URL, username and password."""
101 "User-Agent":
"python-requests/2.32.5",
102 "Connection":
"close",
113 """Set the log intensity level for the client.
115 :param intensity: LogIntensity level to set (Standard or High)
120 """Get the log intensity level for the client.
122 :return: The current LogIntensity level (Standard or High)
127 """Get the authentication method used by the client.
129 :return: The authentication method used by the client
134 """Get the HTTP session used by the client.
136 :return: The requests.Session object used for HTTP communication
141 """Handles the authentication method based on the provided settings."""
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}'"
150 _logger.debug(f
"Authentication method: Basic Auth | '{self.auth_settings.basic_auth.username}'")
155 _logger.debug(
"Authentication method: Bearer Token")
156 self.
_session_session.headers.update({
"Authorization": f
"Bearer {self.auth_settings.bearer_auth.get_token()}"})
160 _logger.debug(
"Authentication method: No Authentication")
163 """Get the root endpoint of the AAS server API to test connectivity.
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.
169 :return: Response data as a dictionary containing shell information, or None if an error occurred
172 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
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")
183 error_messages: dict[int, str] = {}
186 _logger.debug(f
"Testing connectivity with URL: {url}")
189 _logger.debug(f
"Call REST API url '{response.url}'")
191 if response.status_code == STATUS_CODE_200:
192 content = response.content.decode(
"utf-8")
193 return json.loads(content)
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})
198 except requests.exceptions.RequestException
as e:
199 _logger.error(f
"Error call REST API: {e}")
204 """Set authentication token in session headers based on configured authentication method.
206 :return: The access token if set, otherwise None
209 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
223 if token_data
and token_data.access_token:
227 self.
_session_session.headers.update({
"Authorization": f
"Bearer {self._cached_token.access_token}"})
232 def get_endpoint(self, end_point_url: str) ->
None | dict:
233 """Generic GET request for endpoint.
235 :param end_point_url: The endpoint URL to send the GET request to.
236 :return: The base URL of the AAS server.
239 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
244 _logger.debug(f
"Call REST API url '{response.url}'")
246 if response.status_code == STATUS_CODE_200:
247 content = response.content.decode(
"utf-8")
248 return json.loads(content)
250 except requests.exceptions.RequestException
as e:
251 _logger.debug(f
"Error call REST API: {e}")
255 def put_endpoint(self, end_point_url: str, request_body: dict) ->
None | dict:
256 """Generic PUT request for endpoint.
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.
263 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
268 _logger.debug(f
"Call REST API url '{response.url}'")
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)
274 except requests.exceptions.RequestException
as e:
275 _logger.debug(f
"Error call REST API: {e}")
279 def post_endpoint(self, end_point_url: str, request_body: dict) ->
None | dict:
280 """Generic POST request for endpoint.
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.
287 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
292 _logger.debug(f
"Call REST API url '{response.url}'")
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)
298 except requests.exceptions.RequestException
as e:
299 _logger.debug(f
"Error call REST API: {e}")
303 def patch_endpoint(self, end_point_url: str, request_body: dict) ->
None | dict:
304 """Generic PATCH request for endpoint.
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.
311 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
316 _logger.debug(f
"Call REST API url '{response.url}'")
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)
322 except requests.exceptions.RequestException
as e:
323 _logger.debug(f
"Error call REST API: {e}")
328 """Generic DELETE request for endpoint.
330 :param end_point_url: The endpoint URL to send the DELETE request to.
331 :return: The base URL of the AAS server.
334 _logger.error(
"HTTP session is not initialized. Call 'initialize()' method before making API calls.")
339 _logger.debug(f
"Call REST API url '{response.url}'")
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)
345 except requests.exceptions.RequestException
as e:
346 _logger.debug(f
"Error call REST API: {e}")
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 =
"",
362 connection_time_out: int = 60,
363 ssl_verify: bool =
True,
364 trust_env: bool =
True,
365 encoded_ids: bool =
True,
366) -> AasHttpClient |
None:
367 """Create a HTTP client for a AAS server connection from the given parameters.
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
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)
396 config_dict[
"AuthenticationSettings"] = {
397 "BasicAuth": {
"Username": basic_auth_username},
399 "ClientId": o_auth_client_id,
400 "TokenUrl": o_auth_token_url,
404 return create_by_dict(config_dict, basic_auth_password, o_auth_client_secret, bearer_auth_token)
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.
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
418 _logger.info(
"Create AAS server http client from dictionary.")
420 return _create_client(configuration, basic_auth_password, o_auth_client_secret, bearer_auth_token)
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.
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
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():
438 _logger.warning(f
"Configuration file '{config_file}' not found. Using default configuration.")
440 config_string = config_file.read_text(encoding=
"utf-8")
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}")
446 _logger.debug(f
"Configuration file '{config_file}' found.")
448 return _create_client(configuration, basic_auth_password, o_auth_client_secret, bearer_auth_token)
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.
454 This internal method validates the configuration, sets authentication credentials,
455 initializes the client, and tests the connection to the AAS server.
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
466 client = AasHttpClient.model_validate(config_dict)
467 except ValidationError
as ve:
468 raise ValidationError(f
"Invalid BaSyx server configuration file: {ve}")
from ve
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)
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}'.")
487 connected = __connect_to_api(client)
495def __connect_to_api(client: AasHttpClient) -> bool:
496 """Test the connection to the AAS server API with retry logic.
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.
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
506 start_time = time.time()
507 _logger.info(f
"Try to connect to REST API '{client.base_url}' for {client.connection_time_out} seconds.")
511 root = client.get_root()
513 _logger.info(f
"Connected to server API at '{client.base_url}' successfully.")
516 _logger.error(f
"Connection attempt to '{client.base_url}' failed.")
518 except requests.exceptions.ConnectionError:
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.")
524 _logger.warning(f
"Retrying connection (attempt: {counter}).")
Exception raised for errors in the AAS connection.
__init__(self, str message, dict[int, str] errors)
Initialize the AASConnectionError.
Represents a AasHttpClient to communicate with a REST API.
LogIntensity _log_intensity
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
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.
None|dict put_endpoint(self, str end_point_url, dict request_body)
Generic PUT request for endpoint.
ExperimentalImplementation experimental
set_log_intensity(self, LogIntensity intensity)
Set the log intensity level for the client.
AuthenticationConfig auth_settings
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
ShellRepoImplementation shells
ShellRegistryImplementation shell_registry
LogIntensity get_log_intensity(self)
Get the log intensity level for the client.