Skip to content

Commit

Permalink
Merge pull request #318 from RWTH-EBC/281-Intergrade-the-keyValues-en…
Browse files Browse the repository at this point in the history
…dpoints-with-the-normal-ones

281 intergrade the key values endpoints with the normal ones
  • Loading branch information
sbanoeon authored Sep 16, 2024
2 parents a947af0 + 6c082ea commit f8660a2
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 68 deletions.
7 changes: 4 additions & 3 deletions examples/ngsi_v2/e12_ngsi_v2_use_case_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 "
Expand Down Expand Up @@ -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
Expand Down
127 changes: 65 additions & 62 deletions filip/clients/ngsi_v2/cb.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"""
from __future__ import annotations

import copy
from copy import deepcopy
from math import inf
from pkg_resources import parse_version
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -941,33 +939,38 @@ 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)})

try:
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

Expand Down
55 changes: 52 additions & 3 deletions tests/clients/test_ngsi_v2_cb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -918,9 +919,57 @@ 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})
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,
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_entity_key_values(entity=entity1_key_value_dict)
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,
Expand Down

0 comments on commit f8660a2

Please sign in to comment.