From 2a8c55d09aace425264a0ec684d8c047d57f3c70 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 27 Aug 2024 11:44:40 +0200 Subject: [PATCH 1/3] chore: integrate keyvalue update into existing functions --- filip/clients/ngsi_v2/cb.py | 127 ++++++++++++++++--------------- tests/clients/test_ngsi_v2_cb.py | 15 +++- 2 files changed, 76 insertions(+), 66 deletions(-) diff --git a/filip/clients/ngsi_v2/cb.py b/filip/clients/ngsi_v2/cb.py index 8b8455b4..0da0f04a 100644 --- a/filip/clients/ngsi_v2/cb.py +++ b/filip/clients/ngsi_v2/cb.py @@ -3,6 +3,7 @@ """ from __future__ import annotations +import copy from copy import deepcopy from math import inf from pkg_resources import parse_version @@ -264,7 +265,7 @@ def post_entity( entity=entity, override_attr_metadata=override_attr_metadata ) else: - return self.update_entity_key_values(entity=entity) + return self._patch_entity_key_values(entity=entity) msg = f"Could not post entity {entity.id}" self.log_error(err=err, msg=msg) raise @@ -541,7 +542,10 @@ def get_entity_attributes( self.log_error(err=err, msg=msg) raise - def update_entity(self, entity: ContextEntity, append_strict: bool = False): + def update_entity(self, entity: Union[ContextEntity, ContextEntityKeyValues, dict], + append_strict: bool = False, + key_values: bool = False + ): """ The request payload is an object representing the attributes to append or update. @@ -560,15 +564,29 @@ def update_entity(self, entity: ContextEntity, append_strict: bool = False): to that, in case some of the attributes in the payload already exist in the entity, an error is returned. More precisely this means a strict append procedure. - + key_values: By default False. If set to True, the payload uses + the keyValues simplified entity representation, i.e. + ContextEntityKeyValues. Returns: None """ + if key_values: + if isinstance(entity, dict): + entity = copy.deepcopy(entity) + _id = entity.pop("id") + _type = entity.pop("type") + attrs = entity + entity = ContextEntityKeyValues(id=_id, type=_type) + else: + attrs = entity.model_dump(exclude={"id", "type"}) + else: + attrs = entity.get_attributes() self.update_or_append_entity_attributes( entity_id=entity.id, entity_type=entity.type, - attrs=entity.get_attributes(), + attrs=attrs, append_strict=append_strict, + key_values=key_values, ) def update_entity_properties(self, entity: ContextEntity, append_strict: bool = False): @@ -749,11 +767,14 @@ def delete_entities(self, entities: List[ContextEntity]) -> None: def update_or_append_entity_attributes( self, entity_id: str, - attrs: List[Union[NamedContextAttribute, - Dict[str, ContextAttribute]]], + attrs: Union[List[NamedContextAttribute], + Dict[str, ContextAttribute], + Dict[str, Any]], entity_type: str = None, append_strict: bool = False, - forcedUpdate: bool = False): + forcedUpdate: bool = False, + key_values: bool = False + ): """ The request payload is an object representing the attributes to append or update. This corresponds to a 'POST' request if append is @@ -781,6 +802,9 @@ def update_or_append_entity_attributes( subscription, no matter if there is an actual attribute update or no instead of the default behavior, which is to updated only if attribute is effectively updated. + key_values: By default False. If set to True, the payload uses + the keyValues simplified entity representation, i.e. + ContextEntityKeyValues. Returns: None @@ -798,17 +822,23 @@ def update_or_append_entity_attributes( options.append("append") if forcedUpdate: options.append("forcedUpdate") + if key_values: + assert isinstance(attrs, dict), "for keyValues attrs has to be a dict" + options.append("keyValues") if options: params.update({'options': ",".join(options)}) - entity = ContextEntity(id=entity_id, type=entity_type) - entity.add_attributes(attrs) + if key_values: + entity = ContextEntityKeyValues(id=entity_id, type=entity_type, **attrs) + else: + entity = ContextEntity(id=entity_id, type=entity_type) + entity.add_attributes(attrs) # exclude commands from the send data, # as they live in the IoTA-agent excluded_keys = {"id", "type"} - excluded_keys.update( - entity.get_commands(response_format=PropertyFormat.DICT).keys() - ) + # excluded_keys.update( + # entity.get_commands(response_format=PropertyFormat.DICT).keys() + # ) try: res = self.post( url=url, @@ -828,7 +858,7 @@ def update_or_append_entity_attributes( self.log_error(err=err, msg=msg) raise - def update_entity_key_values(self, + def _patch_entity_key_values(self, entity: Union[ContextEntityKeyValues, dict],): """ The entity are updated with a ContextEntityKeyValues object or a @@ -865,51 +895,16 @@ def update_entity_key_values(self, self.log_error(err=err, msg=msg) raise - def update_entity_attributes_key_values(self, - entity_id: str, - attrs: dict, - entity_type: str = None, - ): - """ - Update entity with attributes in keyValues form. - This corresponds to a 'PATcH' request. - Only existing attribute can be updated! - - Args: - entity_id: Entity id to be updated - entity_type: Entity type, to avoid ambiguity in case there are - several entities with the same entity id. - attrs: a dictionary that contains the attribute values. - e.g. { - "temperature": 21.4, - "humidity": 50 - } - - Returns: - - """ - if entity_type: - pass - else: - _entity = self.get_entity(entity_id=entity_id) - entity_type = _entity.type - - entity_dict = attrs.copy() - entity_dict.update({ - "id": entity_id, - "type": entity_type - }) - entity = ContextEntityKeyValues(**entity_dict) - self.update_entity_key_values(entity=entity) - def update_existing_entity_attributes( self, entity_id: str, - attrs: List[Union[NamedContextAttribute, - Dict[str, ContextAttribute]]], + attrs: Union[List[NamedContextAttribute], + Dict[str, ContextAttribute], + Dict[str, Any]], entity_type: str = None, forcedUpdate: bool = False, - override_metadata: bool = False + override_metadata: bool = False, + key_values: bool = False, ): """ The entity attributes are updated with the ones in the payload. @@ -929,6 +924,9 @@ def update_existing_entity_attributes( override_metadata: Bool,replace the existing metadata with the one provided in the request + key_values: By default False. If set to True, the payload uses + the keyValues simplified entity representation, i.e. + ContextEntityKeyValues. Returns: None @@ -941,14 +939,22 @@ def update_existing_entity_attributes( params = None entity_type = "dummy" - entity = ContextEntity(id=entity_id, type=entity_type) - entity.add_attributes(attrs) - options = [] if override_metadata: options.append("overrideMetadata") if forcedUpdate: options.append("forcedUpdate") + if key_values: + assert isinstance(attrs, dict), "for keyValues the attrs must be dict" + payload = attrs + options.append("keyValues") + else: + entity = ContextEntity(id=entity_id, type=entity_type) + entity.add_attributes(attrs) + payload = entity.model_dump( + exclude={"id", "type"}, + exclude_none=True + ) if options: params.update({'options': ",".join(options)}) @@ -956,18 +962,15 @@ def update_existing_entity_attributes( res = self.patch( url=url, headers=headers, - json=entity.model_dump( - exclude={"id", "type"}, - exclude_none=True - ), + json=payload, params=params, ) if res.ok: - self.logger.info("Entity '%s' successfully " "updated!", entity.id) + self.logger.info("Entity '%s' successfully " "updated!", entity_id) else: res.raise_for_status() except requests.RequestException as err: - msg = f"Could not update attributes of entity" f" {entity.id} !" + msg = f"Could not update attributes of entity" f" {entity_id} !" self.log_error(err=err, msg=msg) raise diff --git a/tests/clients/test_ngsi_v2_cb.py b/tests/clients/test_ngsi_v2_cb.py index ad0416d3..4ccf4e1c 100644 --- a/tests/clients/test_ngsi_v2_cb.py +++ b/tests/clients/test_ngsi_v2_cb.py @@ -896,7 +896,7 @@ def test_update_entity_keyvalues(self): # update entity with ContextEntityKeyValues entity1_key_value.temperature = 30 - self.client.update_entity_key_values(entity=entity1_key_value) + self.client.update_entity(entity=entity1_key_value, key_values=True) self.assertEqual(entity1_key_value, self.client.get_entity( entity_id=entity1.id, @@ -909,7 +909,8 @@ def test_update_entity_keyvalues(self): # update entity with dictionary entity1_key_value_dict = entity1_key_value.model_dump() entity1_key_value_dict["temperature"] = 40 - self.client.update_entity_key_values(entity=entity1_key_value_dict) + self.client.update_entity(entity=entity1_key_value_dict, + key_values=True) self.assertEqual(entity1_key_value_dict, self.client.get_entity( entity_id=entity1.id, @@ -918,9 +919,15 @@ def test_update_entity_keyvalues(self): entity3 = self.client.get_entity(entity_id=entity1.id) self.assertEqual(entity1.temperature.type, entity3.temperature.type) + # if attribute not existing, will be created entity1_key_value_dict.update({"humidity": 50}) - with self.assertRaises(RequestException): - self.client.update_entity_key_values(entity=entity1_key_value_dict) + self.client.update_entity(entity=entity1_key_value_dict, + key_values=True) + self.assertEqual(entity1_key_value_dict, + self.client.get_entity( + entity_id=entity1.id, + response_format=AttrsFormat.KEY_VALUES).model_dump() + ) @clean_test(fiware_service=settings.FIWARE_SERVICE, fiware_servicepath=settings.FIWARE_SERVICEPATH, From 70172d9753b9b7f17eb5b6bd60c7c534104f0b8d Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 27 Aug 2024 11:45:34 +0200 Subject: [PATCH 2/3] fix: example --- examples/ngsi_v2/e12_ngsi_v2_use_case_models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py b/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py index 548d21c8..d5bd783d 100644 --- a/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py +++ b/examples/ngsi_v2/e12_ngsi_v2_use_case_models.py @@ -7,6 +7,7 @@ # In short: this workflow shows you a way to keep use case model simple and # reusable while ensuring the compatability with FIWARE NGSI-V2 standards """ +from typing import Optional from pydantic import ConfigDict, BaseModel from pydantic.fields import Field, FieldInfo from filip.models import FiwareHeader @@ -43,11 +44,11 @@ class PostalAddress(BaseModel): alias="streetAddress", description="The street address. For example, 1600 Amphitheatre Pkwy.", ) - address_region: str = Field( + address_region: Optional[str] = Field( alias="addressRegion", default=None, ) - address_locality: str = Field( + address_locality: Optional[str] = Field( alias="addressLocality", default=None, description="The locality in which the street address is, and which is " @@ -125,7 +126,7 @@ class WeatherStationFIWARE(WeatherStation, ContextEntityKeyValues): # 2. Update data weather_station.temperature = 30 # represent use case algorithm - cb_client.update_entity_key_values(entity=weather_station) + cb_client.update_entity(entity=weather_station, key_values=True) # 3. Query and validate data # represent querying data by data users From 6c082ea5955a7ae1eb588115f7db82bdb5edc269 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 27 Aug 2024 11:59:25 +0200 Subject: [PATCH 3/3] test: update attr with keyvalues --- tests/clients/test_ngsi_v2_cb.py | 42 ++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/clients/test_ngsi_v2_cb.py b/tests/clients/test_ngsi_v2_cb.py index 4ccf4e1c..df8f3598 100644 --- a/tests/clients/test_ngsi_v2_cb.py +++ b/tests/clients/test_ngsi_v2_cb.py @@ -929,6 +929,48 @@ def test_update_entity_keyvalues(self): response_format=AttrsFormat.KEY_VALUES).model_dump() ) + @clean_test(fiware_service=settings.FIWARE_SERVICE, + fiware_servicepath=settings.FIWARE_SERVICEPATH, + cb_url=settings.CB_URL) + def test_update_attributes_keyvalues(self): + entity1 = self.entity.model_copy(deep=True) + # initial entity + self.client.post_entity(entity1) + + # update existing attributes + self.client.update_or_append_entity_attributes( + entity_id=entity1.id, + attrs={"temperature": 30}, + key_values=True) + self.assertEqual(30, self.client.get_attribute_value(entity_id=entity1.id, + attr_name="temperature")) + + # update not existing attributes + self.client.update_or_append_entity_attributes( + entity_id=entity1.id, + attrs={"humidity": 40}, + key_values=True) + self.assertEqual(40, self.client.get_attribute_value(entity_id=entity1.id, + attr_name="humidity")) + + # update both existing and not existing attributes + with self.assertRaises(RequestException): + self.client.update_or_append_entity_attributes( + entity_id=entity1.id, + attrs={"humidity": 50, "co2": 300}, + append_strict=True, + key_values=True) + self.client.update_or_append_entity_attributes( + entity_id=entity1.id, + attrs={"humidity": 50, "co2": 300}, + key_values=True) + self.assertEqual(50, self.client.get_attribute_value(entity_id=entity1.id, + attr_name="humidity")) + self.assertEqual(300, self.client.get_attribute_value(entity_id=entity1.id, + attr_name="co2")) + self.assertEqual(30, self.client.get_attribute_value(entity_id=entity1.id, + attr_name="temperature")) + @clean_test(fiware_service=settings.FIWARE_SERVICE, fiware_servicepath=settings.FIWARE_SERVICEPATH, cb_url=settings.CB_URL)