diff --git a/filip/clients/exceptions.py b/filip/clients/exceptions.py new file mode 100644 index 00000000..18386a3d --- /dev/null +++ b/filip/clients/exceptions.py @@ -0,0 +1,17 @@ +""" +Module for client specific exceptions +""" +import requests.models + + +class BaseHttpClientException(Exception): + """ + Base exception class for all HTTP clients. The response of a request will be available in the exception. + + Args: + message (str): Error message + response (Response): Response object + """ + def __init__(self, message: str, response: requests.models.Response): + super().__init__(message) + self.response = response diff --git a/filip/clients/ngsi_v2/cb.py b/filip/clients/ngsi_v2/cb.py index 8b8455b4..4432696a 100644 --- a/filip/clients/ngsi_v2/cb.py +++ b/filip/clients/ngsi_v2/cb.py @@ -32,6 +32,7 @@ from filip.models.ngsi_v2.base import AttrsFormat from filip.models.ngsi_v2.subscriptions import Subscription, Message from filip.models.ngsi_v2.registrations import Registration +from filip.clients.exceptions import BaseHttpClientException if TYPE_CHECKING: from filip.clients.ngsi_v2.iota import IoTAClient @@ -157,7 +158,7 @@ def get_version(self) -> Dict: res.raise_for_status() except requests.RequestException as err: self.logger.error(err) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def get_resources(self) -> Dict: """ @@ -174,7 +175,7 @@ def get_resources(self) -> Dict: res.raise_for_status() except requests.RequestException as err: self.logger.error(err) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err # STATISTICS API def get_statistics(self) -> Dict: @@ -191,7 +192,7 @@ def get_statistics(self) -> Dict: res.raise_for_status() except requests.RequestException as err: self.logger.error(err) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err # CONTEXT MANAGEMENT API ENDPOINTS # Entity Operations @@ -266,8 +267,7 @@ def post_entity( else: return self.update_entity_key_values(entity=entity) msg = f"Could not post entity {entity.id}" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_entity_list( self, @@ -412,8 +412,7 @@ def get_entity_list( except requests.RequestException as err: msg = "Could not load entities" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_entity( self, @@ -475,8 +474,7 @@ def get_entity( res.raise_for_status() except requests.RequestException as err: msg = f"Could not load entity {entity_id}" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_entity_attributes( self, @@ -538,8 +536,7 @@ def get_entity_attributes( res.raise_for_status() except requests.RequestException as err: msg = f"Could not load attributes from entity {entity_id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_entity(self, entity: ContextEntity, append_strict: bool = False): """ @@ -677,8 +674,7 @@ def delete_entity( res.raise_for_status() except requests.RequestException as err: msg = f"Could not delete entity {entity_id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err if delete_devices: from filip.clients.ngsi_v2 import IoTAClient @@ -825,8 +821,7 @@ def update_or_append_entity_attributes( res.raise_for_status() except requests.RequestException as err: msg = f"Could not update or append attributes of entity" f" {entity.id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_entity_key_values(self, entity: Union[ContextEntityKeyValues, dict],): @@ -862,8 +857,7 @@ def update_entity_key_values(self, except requests.RequestException as err: msg = f"Could not update attributes of entity" \ f" {entity.id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_entity_attributes_key_values(self, entity_id: str, @@ -968,8 +962,7 @@ def update_existing_entity_attributes( res.raise_for_status() except requests.RequestException as err: msg = f"Could not update attributes of entity" f" {entity.id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def override_entity(self, entity: Union[ContextEntity, ContextEntityKeyValues], @@ -1064,8 +1057,7 @@ def replace_entity_attributes( res.raise_for_status() except requests.RequestException as err: msg = f"Could not replace attribute of entity {entity_id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # Attribute operations def get_attribute( @@ -1111,8 +1103,7 @@ def get_attribute( msg = ( f"Could not load attribute '{attr_name}' from entity" f"'{entity_id}' " ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_entity_attribute(self, entity_id: str, @@ -1197,8 +1188,7 @@ def update_entity_attribute(self, msg = ( f"Could not update attribute '{attr_name}' of entity" f"'{entity_id}' " ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def delete_entity_attribute( self, entity_id: str, attr_name: str, entity_type: str = None @@ -1234,8 +1224,7 @@ def delete_entity_attribute( msg = ( f"Could not delete attribute '{attr_name}' of entity '{entity_id}'" ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # Attribute value operations def get_attribute_value( @@ -1271,8 +1260,7 @@ def get_attribute_value( f"Could not load value of attribute '{attr_name}' from " f"entity'{entity_id}' " ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_attribute_value(self, *, entity_id: str, @@ -1329,8 +1317,7 @@ def update_attribute_value(self, *, f"Could not update value of attribute '{attr_name}' from " f"entity '{entity_id}' " ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # Types Operations def get_entity_types( @@ -1363,8 +1350,7 @@ def get_entity_types( res.raise_for_status() except requests.RequestException as err: msg = "Could not load entity types!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_entity_type(self, entity_type: str) -> Dict[str, Any]: """ @@ -1386,8 +1372,7 @@ def get_entity_type(self, entity_type: str) -> Dict[str, Any]: res.raise_for_status() except requests.RequestException as err: msg = f"Could not load entities of type" f"'{entity_type}' " - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # SUBSCRIPTION API ENDPOINTS def get_subscription_list(self, limit: PositiveInt = inf) -> List[Subscription]: @@ -1413,8 +1398,7 @@ def get_subscription_list(self, limit: PositiveInt = inf) -> List[Subscription]: return adapter.validate_python(items) except requests.RequestException as err: msg = "Could not load subscriptions!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def post_subscription( self, @@ -1498,8 +1482,7 @@ def post_subscription( res.raise_for_status() except requests.RequestException as err: msg = "Could not send subscription!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_subscription(self, subscription_id: str) -> Subscription: """ @@ -1520,8 +1503,7 @@ def get_subscription(self, subscription_id: str) -> Subscription: res.raise_for_status() except requests.RequestException as err: msg = f"Could not load subscription {subscription_id}" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_subscription( self, subscription: Subscription, skip_initial_notification: bool = False @@ -1574,8 +1556,7 @@ def update_subscription( res.raise_for_status() except requests.RequestException as err: msg = f"Could not update subscription {subscription.id}" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def delete_subscription(self, subscription_id: str) -> None: """ @@ -1595,8 +1576,7 @@ def delete_subscription(self, subscription_id: str) -> None: res.raise_for_status() except requests.RequestException as err: msg = f"Could not delete subscription {subscription_id}" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # Registration API def get_registration_list(self, *, limit: PositiveInt = None) -> List[Registration]: @@ -1623,8 +1603,7 @@ def get_registration_list(self, *, limit: PositiveInt = None) -> List[Registrati return adapter.validate_python(items) except requests.RequestException as err: msg = "Could not load registrations!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def post_registration(self, registration: Registration): """ @@ -1653,8 +1632,7 @@ def post_registration(self, registration: Registration): res.raise_for_status() except requests.RequestException as err: msg = f"Could not send registration {registration.id}!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def get_registration(self, registration_id: str) -> Registration: """ @@ -1676,8 +1654,7 @@ def get_registration(self, registration_id: str) -> Registration: res.raise_for_status() except requests.RequestException as err: msg = f"Could not load registration {registration_id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_registration(self, registration: Registration): """ @@ -1706,8 +1683,7 @@ def update_registration(self, registration: Registration): res.raise_for_status() except requests.RequestException as err: msg = f"Could not update registration {registration.id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def delete_registration(self, registration_id: str) -> None: """ @@ -1726,8 +1702,7 @@ def delete_registration(self, registration_id: str) -> None: res.raise_for_status() except requests.RequestException as err: msg = f"Could not delete registration {registration_id} !" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # Batch operation API def update(self, @@ -1810,8 +1785,7 @@ def update(self, res.raise_for_status() except requests.RequestException as err: msg = f"Update operation '{action_type}' failed!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def query( self, @@ -1861,8 +1835,7 @@ def query( return items except requests.RequestException as err: msg = "Query operation failed!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def notify(self, message: Message) -> None: """ @@ -1900,8 +1873,7 @@ def notify(self, message: Message) -> None: f"Sending notifcation message failed! \n " f"{message.model_dump_json(indent=2)}" ) - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def post_command( self, diff --git a/filip/clients/ngsi_v2/iota.py b/filip/clients/ngsi_v2/iota.py index 49cd28ed..c8adb2fa 100644 --- a/filip/clients/ngsi_v2/iota.py +++ b/filip/clients/ngsi_v2/iota.py @@ -13,6 +13,7 @@ from pydantic.type_adapter import TypeAdapter from filip.config import settings from filip.clients.base_http_client import BaseHttpClient +from filip.clients.exceptions import BaseHttpClientException from filip.models.base import FiwareHeader from filip.models.ngsi_v2.iot import Device, ServiceGroup @@ -117,8 +118,7 @@ def post_groups(self, else: res.raise_for_status() except requests.RequestException as err: - self.log_error(err=err, msg=None) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def post_group(self, service_group: ServiceGroup, update: bool = False): """ @@ -154,8 +154,7 @@ def get_group_list(self) -> List[ServiceGroup]: return ta.validate_python(res.json()['services']) res.raise_for_status() except requests.RequestException as err: - self.log_error(err=err, msg=None) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def get_group(self, *, resource: str, apikey: str) -> ServiceGroup: """ @@ -236,8 +235,7 @@ def update_group(self, *, service_group: ServiceGroup, else: res.raise_for_status() except requests.RequestException as err: - self.log_error(err=err, msg=None) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def delete_group(self, *, resource: str, apikey: str): """ @@ -265,8 +263,7 @@ def delete_group(self, *, resource: str, apikey: str): except requests.RequestException as err: msg = f"Could not delete ServiceGroup with resource " \ f"'{resource}' and apikey '{apikey}'!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # DEVICE API def post_devices(self, *, devices: Union[Device, List[Device]], @@ -299,8 +296,7 @@ def post_devices(self, *, devices: Union[Device, List[Device]], if update: return self.update_devices(devices=devices, add=False) msg = "Could not post devices" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def post_device(self, *, device: Device, update: bool = False) -> None: """ @@ -368,8 +364,7 @@ def get_device_list(self, *, return devices res.raise_for_status() except requests.RequestException as err: - self.log_error(err=err, msg=None) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def get_device(self, *, device_id: str) -> Device: """ @@ -392,8 +387,7 @@ def get_device(self, *, device_id: str) -> Device: res.raise_for_status() except requests.RequestException as err: msg = f"Device {device_id} was not found" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_device(self, *, device: Device, add: bool = True) -> None: """ @@ -425,8 +419,7 @@ def update_device(self, *, device: Device, add: bool = True) -> None: res.raise_for_status() except requests.RequestException as err: msg = f"Could not update device '{device.device_id}'" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def update_devices(self, *, devices: Union[Device, List[Device]], add: False) -> None: @@ -492,8 +485,7 @@ def delete_device(self, *, device_id: str, res.raise_for_status() except requests.RequestException as err: msg = f"Could not delete device {device_id}!" - self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err if delete_entity: # An entity can technically belong to multiple devices @@ -506,6 +498,7 @@ def delete_device(self, *, device_id: str, f"{device_id} was not deleted because it is " f"linked to multiple devices. ") else: + cb_client_local = None try: from filip.clients.ngsi_v2 import ContextBrokerClient @@ -513,7 +506,7 @@ def delete_device(self, *, device_id: str, cb_client_local = deepcopy(cb_client) else: warnings.warn("No `ContextBrokerClient` " - "object providesd! Will try to generate " + "object provided! Will try to generate " "one. This usage is not recommended.") cb_client_local = ContextBrokerClient( @@ -531,7 +524,8 @@ def delete_device(self, *, device_id: str, # this methode, not if this methode actively deleted it pass - cb_client_local.close() + if cb_client_local: + cb_client_local.close() def patch_device(self, device: Device, diff --git a/filip/clients/ngsi_v2/quantumleap.py b/filip/clients/ngsi_v2/quantumleap.py index f9471f83..36c63155 100644 --- a/filip/clients/ngsi_v2/quantumleap.py +++ b/filip/clients/ngsi_v2/quantumleap.py @@ -22,6 +22,7 @@ AttributeValues, \ TimeSeries, \ TimeSeriesHeader +from filip.clients.exceptions import BaseHttpClientException logger = logging.getLogger(__name__) @@ -71,7 +72,7 @@ def get_version(self) -> Dict: res.raise_for_status() except requests.exceptions.RequestException as err: self.logger.error(err) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def get_health(self) -> Dict: """ @@ -94,7 +95,7 @@ def get_health(self) -> Dict: res.raise_for_status() except requests.exceptions.RequestException as err: self.logger.error(err) - raise + raise BaseHttpClientException(message=err.response.text, response=err.response) from err def post_config(self): """ @@ -134,7 +135,7 @@ def post_notification(self, notification: Message): msg = f"Could not post notification for subscription id " \ f"{notification.subscriptionId}" self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err def post_subscription(self, cb_url: Union[AnyHttpUrl, str], @@ -256,7 +257,7 @@ def delete_entity_type(self, entity_type: str) -> str: except requests.exceptions.RequestException as err: msg = f"Could not delete entities of type {entity_type}" self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err # QUERY API ENDPOINTS def __query_builder(self, @@ -392,7 +393,7 @@ def __query_builder(self, else: msg = "Could not load entity data" self.log_error(err=err, msg=msg) - raise + raise BaseHttpClientException(message=msg, response=err.response) from err self.logger.info("Successfully retrieved entity data") return res_q diff --git a/tests/clients/test_ngsi_v2_cb.py b/tests/clients/test_ngsi_v2_cb.py index ad0416d3..93a9ee07 100644 --- a/tests/clients/test_ngsi_v2_cb.py +++ b/tests/clients/test_ngsi_v2_cb.py @@ -27,6 +27,7 @@ Query, \ ActionType, \ ContextEntityKeyValues +from filip.clients.exceptions import BaseHttpClientException from filip.models.ngsi_v2.base import AttrsFormat, EntityPattern, Status, \ NamedMetadata @@ -1897,6 +1898,18 @@ def test_does_entity_exist(self): entity_type=entity.type) ) + @clean_test(fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL) + def test_entity_exceptions(self): + entity1 = self.entity.model_copy(deep=True) + self.client.post_entity(entity1) + + with self.assertRaises(BaseHttpClientException) as context: + self.client.post_entity(entity1) + self.assertEqual(json.loads(context.exception.response.text)["description"], "Already Exists") + + def tearDown(self) -> None: """ Cleanup test server diff --git a/tests/clients/test_ngsi_v2_iota.py b/tests/clients/test_ngsi_v2_iota.py index 8518a285..156ce1b4 100644 --- a/tests/clients/test_ngsi_v2_iota.py +++ b/tests/clients/test_ngsi_v2_iota.py @@ -5,6 +5,7 @@ import unittest import logging import requests +import json from uuid import uuid4 @@ -12,6 +13,7 @@ from filip.clients.ngsi_v2 import \ ContextBrokerClient, \ IoTAClient +from filip.clients.exceptions import BaseHttpClientException from filip.models.ngsi_v2.iot import \ ServiceGroup, \ Device, \ @@ -380,6 +382,32 @@ def test_patch_device(self): new_device.__getattribute__(key)) cb_client.close() + @clean_test(fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL, + iota_url=settings.IOTA_JSON_URL) + def test_device_exceptions(self): + """ + Test for exceptions when handling a Device + """ + with IoTAClient(url=settings.IOTA_JSON_URL, fiware_header=self.fiware_header) as client: + device = Device(**self.device) + client.post_device(device=device) + + with self.assertRaises(BaseHttpClientException) as context: + client.post_device(device=device) + self.assertEqual(json.loads(context.exception.response.text)["name"], "DUPLICATE_DEVICE_ID") + + with self.assertRaises(BaseHttpClientException) as context: + client.update_device(device=device, add=False) + self.assertEqual(json.loads(context.exception.response.text)["name"], "ENTITY_GENERIC_ERROR") + + client.delete_device(device_id=device.device_id) + + with self.assertRaises(BaseHttpClientException) as context: + client.delete_device(device_id=device.device_id) + self.assertEqual(json.loads(context.exception.response.text)["name"], "DEVICE_NOT_FOUND") + def test_service_group(self): """ Test of querying service group based on apikey and resource. @@ -467,4 +495,3 @@ def tearDown(self) -> None: clear_all(fiware_header=self.fiware_header, cb_url=settings.CB_URL, iota_url=settings.IOTA_JSON_URL) -