Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update python api client #193

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
158 changes: 117 additions & 41 deletions python/src/aica_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self, url: str = 'localhost', port: Union[str, int] = '5000'):
self._address = f'http://{url}:{port}'

self._logger = getLogger(__name__)
self._api_version = None
self._core_version = None

def _endpoint(self, endpoint=''):
"""
Expand All @@ -58,7 +58,7 @@ def _requires_api_version(version):
Elides the function call and returns None with a warning if the version constraint is violated.

Example usage:
@_requires_api_version('>=3.2.1')
@_requires_core_version('>=3.2.1')
domire8 marked this conversation as resolved.
Show resolved Hide resolved
def my_new_endpoint()
...

Expand All @@ -67,32 +67,64 @@ def my_new_endpoint()
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
if self._api_version is None and self.api_version() is None:
if self._core_version is None and self.api_version() is None:
return None
if not semver.match(self._api_version, version):
if not semver.match(self._core_version, version):
self._logger.warning(f'The function {func.__name__} requires API server version {version}, '
f'but the current API server version is {self._api_version}')
f'but the current API server version is {self._core_version}')
return None
return func(self, *args, **kwargs)

return wrapper

return decorator

@deprecated(deprecated_in='3.0.0', removed_in='4.0.0', current_version=CLIENT_VERSION,
details='Use the core_version function instead')
def api_version(self) -> Union[str, None]:
"""
Get the version of the AICA API server

:return: The version of the API server or None in case of
:return: The version of the API server or None in case of connection failure
"""
try:
self._api_version = requests.get(f'{self._address}/version').json()
self._logger.debug(f'API server version identified as {self._api_version}')
self._core_version = requests.get(f'{self._address}/version').json()
self._logger.debug(f'API server version identified as {self._core_version}')
except requests.exceptions.RequestException:
self._logger.error(f'Error connecting to the API server at {self._address}! '
f'Check that the AICA container is running and configured with the right address.')
self._api_version = None
return self._api_version
self._core_version = None
return self._core_version

def core_version(self) -> Union[str, None]:
"""
Get the version of the AICA Core

:return: The version of the AICA core or None in case of connection failure
"""
try:
self._core_version = requests.get(f'{self._address}/version').json()
self._logger.debug(f'AICA Core version identified as {self._core_version}')
except requests.exceptions.RequestException:
self._logger.error(f'Error connecting to the API server at {self._address}! '
f'Check that the AICA Core is running and configured with the right address.')
self._core_version = None
return self._core_version

def protocol(self) -> Union[str, None]:
"""
Get the version of the API protocol

:return: The version of the API protocol or None in case of connection failure
"""
try:
protocol = requests.get(f'{self._address}/protocol').json()
self._logger.debug(f'API protocol version identified as {protocol}')
return protocol
except requests.exceptions.RequestException:
self._logger.error(f'Error connecting to the API server at {self._address}! '
f'Check that the AICA Core is running and configured with the right address.')
return None

@staticmethod
def client_version() -> str:
Expand All @@ -105,30 +137,35 @@ def client_version() -> str:

def check(self) -> bool:
"""
Check if this API client is compatible with the detected API server version
Check if this API client is compatible with the detected AICA Core version

:return: True if the client is compatible with the API server version, False otherwise
:return: True if the client is compatible with the AICA Core version, False otherwise
"""
if self._api_version is None and self.api_version() is None:
if self._core_version is None and self.api_version() is None:
return False

version_info = semver.parse_version_info(self._api_version)
version_info = semver.parse_version_info(self._core_version)

if version_info.major == 3:
if version_info.major == 4:
return True
elif version_info.major > 3:
self._logger.error(f'The detected API version v{self._api_version} is newer than the maximum API version '
f'supported by this client (v{self.client_version()}). Please upgrade the Python API '
f'client version for newer API server versions.')
elif version_info.major > 4:
self._logger.error(f'The detected AICA Core version v{self._core_version} is newer than the maximum AICA '
f'Core version supported by this client (v{self.client_version()}). Please upgrade the '
f'Python API client version for newer API server versions.')
return False
elif version_info.major == 3:
self._logger.error(f'The detected AICA Core version v{self._core_version} is older than the minimum AICA '
f'Core version supported by this client (v{self.client_version()}). Please downgrade '
f'the Python API client to version v2.1.0 for API server versions v3.X.')
return False
elif version_info.major == 2:
self._logger.error(f'The detected API version v{self._api_version} is older than the minimum API version '
f'supported by this client (v{self.client_version()}). Please downgrade the Python API '
f'client to version v1.2.0 for API server versions v2.X.')
self._logger.error(f'The detected AICA Core version v{self._core_version} is older than the minimum AICA '
f'Core version supported by this client (v{self.client_version()}). Please downgrade '
f'the Python API client to version v1.2.0 for API server versions v2.X.')
return False
else:
self._logger.error(f'The detected API version v{self._api_version} is deprecated and not supported by '
f'this API client!')
self._logger.error(f'The detected AICA Core version v{self._core_version} is deprecated and not supported '
f'by this API client!')
return False

def component_descriptions(self) -> requests.Response:
Expand All @@ -143,6 +180,8 @@ def controller_descriptions(self) -> requests.Response:
"""
return requests.get(self._endpoint('controllers'))

@deprecated(deprecated_in='3.0.0', removed_in='4.0.0', current_version=CLIENT_VERSION,
details='Use the call_component_service function instead')
def call_service(self, component: str, service: str, payload: str) -> requests.Response:
"""
Call a service on a component.
Expand All @@ -155,6 +194,31 @@ def call_service(self, component: str, service: str, payload: str) -> requests.R
data = {"payload": payload}
return requests.put(self._endpoint(endpoint), json=data)

def call_component_service(self, component: str, service: str, payload: str) -> requests.Response:
domire8 marked this conversation as resolved.
Show resolved Hide resolved
"""
Call a service on a component.

:param component: The name of the component
:param service: The name of the service
:param payload: The service payload, formatted according to the respective service description
"""
endpoint = 'application/components/' + component + '/service/' + service
data = {"payload": payload}
return requests.put(self._endpoint(endpoint), json=data)

def call_controller_service(self, hardware: str, controller: str, service: str, payload: str) -> requests.Response:
"""
Call a service on a controller.

:param hardware: The name of the hardware interface
:param controller: The name of the controller
:param service: The name of the service
:param payload: The service payload, formatted according to the respective service description
"""
endpoint = 'application/hardware/' + hardware + 'controller/' + controller + '/service/' + service
data = {"payload": payload}
return requests.put(self._endpoint(endpoint), json=data)

def get_application_state(self) -> requests.Response:
"""
Get the application state
Expand Down Expand Up @@ -258,7 +322,8 @@ def set_controller_parameter(self, hardware: str, controller: str, parameter: st
def set_lifecycle_transition(self, component: str, transition: str) -> requests.Response:
"""
Trigger a lifecycle transition on a component. The transition label must be one of the following:
['configure', 'activate', 'deactivate', 'cleanup', 'shutdown']
['configure', 'activate', 'deactivate', 'cleanup', 'unconfigured_shutdown', 'inactive_shutdown',
'acitve_shutdown']

The transition will only be executed if the target is a lifecycle component and the current lifecycle state
allows the requested transition.
Expand Down Expand Up @@ -324,6 +389,18 @@ def get_application(self):
endpoint = "application"
return requests.get(self._endpoint(endpoint))

def manage_sequence(self, sequence_name: str, action: str):
"""
Manage a sequence. The action label must be one of the following: ['start', 'restart', 'abort']

The action will only be executed if the sequence exists and allows the requested action.

:param sequence_name: The name of the sequence
:param action: The sequence action label
"""
endpoint = f"application/sequences/{sequence_name}?action={action}"
return requests.put(self._endpoint(endpoint))

def wait_for_component(self, component: str, state: str, timeout: Union[None, int, float] = None) -> bool:
"""
Wait for a component to be in a particular state. Components can be in any of the following states:
Expand Down Expand Up @@ -356,7 +433,7 @@ def wait_for_controller(self, hardware: str, controller: str, state: str,
timeout: Union[None, int, float] = None) -> bool:
"""
Wait for a controller to be in a particular state. Controllers can be in any of the following states:
['unloaded', 'loaded', 'unconfigured', 'inactive', 'active', 'finalized']
['unloaded', 'loaded', 'active', 'finalized']

:param hardware: The name of the hardware interface responsible for the controller
:param controller: The name of the controller
Expand All @@ -367,20 +444,6 @@ def wait_for_controller(self, hardware: str, controller: str, state: str,
return read_until(lambda data: data[hardware]['controllers'][controller]['state'] == state, url=self._address,
namespace='/v2/hardware', event='hardware_data', timeout=timeout) is not None

@deprecated(deprecated_in='2.1.0', removed_in='3.0.0', current_version=CLIENT_VERSION,
details='Use the wait_for_component_predicate function instead')
def wait_for_predicate(self, component: str, predicate: str, timeout: Union[None, int, float] = None) -> bool:
"""
Wait until a component predicate is true.

:param component: The name of the component
:param predicate: The name of the predicate
:param timeout: Timeout duration in seconds. If set to None, block indefinitely
:return: True if the predicate is true before the timeout duration, False otherwise
"""
return read_until(lambda data: data[component]['predicates'][predicate], url=self._address,
namespace='/v2/components', event='component_data', timeout=timeout) is not None

@_requires_api_version('>=3.1.0')
def wait_for_component_predicate(self, component: str, predicate: str,
timeout: Union[None, int, float] = None) -> bool:
Expand Down Expand Up @@ -411,7 +474,7 @@ def wait_for_controller_predicate(self, hardware: str, controller: str, predicat
url=self._address, namespace='/v2/hardware', event='hardware_data',
timeout=timeout) is not None

def wait_for_condition(self, condition, timeout=None) -> bool:
def wait_for_condition(self, condition: str, timeout=None) -> bool:
"""
Wait until a condition is true.

Expand All @@ -421,3 +484,16 @@ def wait_for_condition(self, condition, timeout=None) -> bool:
"""
return read_until(lambda data: data[condition], url=self._address, namespace='/v2/conditions',
event='conditions', timeout=timeout) is not None

def wait_for_sequence(self, sequence: str, state: str, timeout=None) -> bool:
"""
Wait for a sequence to be in a particular state. Sequences can be in any of the following states:
['active', 'inactive', 'aborted']

:param sequence: The name of the sequence
:param state: The state of the sequence to wait for
:param timeout: Timeout duration in seconds. If set to None, block indefinitely
:return: True if the condition is true before the timeout duration, False otherwise
"""
return read_until(lambda data: data[sequence]['state'] == state, url=self._address, namespace='/v2/sequences',
event='sequences', timeout=timeout) is not None