From c18f4a2a391a8f62d376b70ab72f328ad3fe6bf1 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 25 Jun 2024 10:51:30 +0200 Subject: [PATCH 1/5] chore: add test for alteration types model --- tests/models/test_ngsi_v2_subscriptions.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/models/test_ngsi_v2_subscriptions.py b/tests/models/test_ngsi_v2_subscriptions.py index 6e9d453a..e107c45c 100644 --- a/tests/models/test_ngsi_v2_subscriptions.py +++ b/tests/models/test_ngsi_v2_subscriptions.py @@ -12,7 +12,7 @@ Mqtt, \ MqttCustom, \ Notification, \ - Subscription + Subscription, Condition from filip.models.base import FiwareHeader from filip.utils.cleanup import clear_all, clean_test from tests.config import settings @@ -181,6 +181,22 @@ def test_model_dump_json(self): with self.assertRaises(KeyError): _ = test_dict["status"] + def test_alteration_types_model(self): + c = Condition(alterationTypes=["entityCreate", "entityDelete"]) + # entity override is not a valid alteration type + with self.assertRaises(ValueError): + c = Condition(alterationTypes=["entityOverride", "entityDelete"]) + # test alteration types with different input types + # list success + c = Condition(alterationTypes=["entityCreate", "entityDelete"]) + # tuple success + c = Condition(alterationTypes=("entityChange", "entityDelete")) + # set success + c = Condition(alterationTypes={"entityUpdate", "entityDelete"}) + # str fail + with self.assertRaises(ValueError): + c = Condition(alterationTypes="entityCreate") + def tearDown(self) -> None: """ Cleanup test server From 204b592d58217f9ab340c59c9a6456b02ebf7d46 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 25 Jun 2024 11:43:27 +0200 Subject: [PATCH 2/5] feat: implement alterationtypes in data model --- filip/models/ngsi_v2/subscriptions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/filip/models/ngsi_v2/subscriptions.py b/filip/models/ngsi_v2/subscriptions.py index f0be6504..0f6c1c8d 100644 --- a/filip/models/ngsi_v2/subscriptions.py +++ b/filip/models/ngsi_v2/subscriptions.py @@ -274,6 +274,8 @@ class Condition(BaseModel): expression matches. If neither attrs nor expression are used, a notification is sent whenever any of the attributes of the entity changes. + alterationTypes: for more information about this field, see + https://github.com/telefonicaid/fiware-orion/blob/3.8.0/doc/manuals/orion-api.md#subscriptions-based-in-alteration-type """ attrs: Optional[Union[str, List[str]]] = Field( @@ -286,6 +288,10 @@ class Condition(BaseModel): 'coords (see "List entities" operation above about this ' 'field).' ) + alterationTypes: Optional[List[str]] = Field( + default=None, + description='list of alteration types triggering the subscription' + ) @field_validator('attrs') def check_attrs(cls, v): @@ -296,6 +302,20 @@ def check_attrs(cls, v): else: raise TypeError() + @field_validator('alterationTypes') + def check_alteration_types(cls, v): + allowed_types = {"entityCreate", "entityDelete", "entityUpdate", "entityChange"} + + if v is None: + return None + elif isinstance(v, list): + for item in v: + if item not in allowed_types: + raise ValueError(f'{item} is not a valid alterationType') + return v + else: + raise ValueError('alterationTypes must be a list of strings') + class Subject(BaseModel): """ From d4c07249170eecaebcd91b00bc146f77a694c9d4 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 25 Jun 2024 11:43:49 +0200 Subject: [PATCH 3/5] chore: add test for the behavior of alterationtypes --- tests/clients/test_ngsi_v2_cb.py | 98 +++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/clients/test_ngsi_v2_cb.py b/tests/clients/test_ngsi_v2_cb.py index a4b7fbf9..6de3a0ef 100644 --- a/tests/clients/test_ngsi_v2_cb.py +++ b/tests/clients/test_ngsi_v2_cb.py @@ -30,7 +30,8 @@ from filip.models.ngsi_v2.base import AttrsFormat, EntityPattern, Status, \ NamedMetadata -from filip.models.ngsi_v2.subscriptions import Mqtt, Message, Subscription +from filip.models.ngsi_v2.subscriptions import Mqtt, Message, Subscription, Condition, \ + Notification from filip.models.ngsi_v2.iot import \ Device, \ DeviceCommand, \ @@ -667,6 +668,101 @@ def test_subscription_set_status(self): sub_res_expired = client.get_subscription(subscription_id=sub_id) self.assertEqual(sub_res_expired.status, Status.EXPIRED) + @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_subscription_alterationtypes(self): + """ + Test behavior of subscription alterationtypes since Orion 3.7.0 + """ + sub = self.subscription.model_copy() + sub.subject.condition = Condition(alterationTypes=[]) + sub.notification = Notification( + mqtt=Mqtt( + url=settings.MQTT_BROKER_URL_INTERNAL, + topic="test/alterationtypes")) + test_entity = ContextEntity(id="test:alterationtypes", type="Room", + temperature={"type": "Number", + "value": 25.0} + ) + + # test default with empty alterationTypes, triggered during actual change + self.client.post_entity(test_entity) + sub_id_default = self.client.post_subscription(subscription=sub) + test_entity = self.client.get_entity(entity_id=test_entity.id) + test_entity.temperature.value = 26.0 + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_default = self.client.get_subscription(sub_id_default) + self.assertEqual(sub_result_default.notification.timesSent, 1) + # not triggered during with no actual update + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_default = self.client.get_subscription(sub_id_default) + self.assertEqual(sub_result_default.notification.timesSent, 1) + self.client.delete_subscription(sub_id_default) + + # test entityChange + sub.subject.condition.alterationTypes = ["entityChange"] + sub_id_change = self.client.post_subscription(subscription=sub) + test_entity.temperature.value = 27.0 + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_change = self.client.get_subscription(sub_id_change) + self.assertEqual(sub_result_change.notification.timesSent, 1) + # not triggered during with no actual update + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_change = self.client.get_subscription(sub_id_change) + self.assertEqual(sub_result_change.notification.timesSent, 1) + self.client.delete_subscription(sub_id_change) + + # test entityCreate + test_entity_create = ContextEntity(id="test:alterationtypes2", type="Room", + temperature={"type": "Number", + "value": 25.0} + ) + sub.subject.condition.alterationTypes = ["entityCreate"] + sub_id_create = self.client.post_subscription(subscription=sub) + self.client.post_entity(test_entity_create) + time.sleep(1) + sub_result_create = self.client.get_subscription(sub_id_create) + self.assertEqual(sub_result_create.notification.timesSent, 1) + # not triggered during when update + test_entity_create = self.client.get_entity(entity_id=test_entity_create.id) + test_entity_create.temperature.value = 26.0 + self.client.update_entity(test_entity_create) + time.sleep(1) + sub_result_create = self.client.get_subscription(sub_id_create) + self.assertEqual(sub_result_create.notification.timesSent, 1) + self.client.delete_subscription(sub_id_create) + + # test entityDelete + sub.subject.condition.alterationTypes = ["entityDelete"] + sub_id_delete = self.client.post_subscription(subscription=sub) + self.client.delete_entity(test_entity_create.id) + time.sleep(1) + sub_result_delete = self.client.get_subscription(sub_id_delete) + self.assertEqual(sub_result_delete.notification.timesSent, 1) + self.client.delete_subscription(sub_id_delete) + + # test entityUpdate (no matter if the entity actually changed or not) + sub.subject.condition.alterationTypes = ["entityUpdate"] + sub_id_update = self.client.post_subscription(subscription=sub) + # triggered when actual change + test_entity.temperature.value = 28.0 + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_update = self.client.get_subscription(sub_id_update) + self.assertEqual(sub_result_update.notification.timesSent, 1) + # triggered when no actual change + self.client.update_entity(test_entity) + time.sleep(1) + sub_result_update = self.client.get_subscription(sub_id_update) + self.assertEqual(sub_result_update.notification.timesSent, 2) + self.client.delete_subscription(sub_id_update) + @clean_test(fiware_service=settings.FIWARE_SERVICE, fiware_servicepath=settings.FIWARE_SERVICEPATH, cb_url=settings.CB_URL, From f5739e2d9f988e80c64d7f62d024d5c2cac2a992 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Tue, 25 Jun 2024 11:45:57 +0200 Subject: [PATCH 4/5] docs: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 815ea3ba..b60ddb52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### v0.4.2 - update: allow duplicated name in device, check uniqueness of object_id ([#279](https://github.com/RWTH-EBC/FiLiP/pull/279)) +- add: support alterationTypes in subscription model ([#293](https://github.com/RWTH-EBC/FiLiP/pull/293)) - add: validation for JEXL based expression ([#260](https://github.com/RWTH-EBC/FiLiP/pull/260)) - add: tutorials for multi-entity ([#260](https://github.com/RWTH-EBC/FiLiP/pull/260)) - add: add ``update_entity_relationships`` to allow relationship update ([#271](https://github.com/RWTH-EBC/FiLiP/pull/271)) From 50c990d62f7497f200bc8f5fc7ea773bee8fd252 Mon Sep 17 00:00:00 2001 From: JunsongDu Date: Thu, 18 Jul 2024 13:26:19 +0200 Subject: [PATCH 5/5] chore: add hints about supported alteration types --- filip/models/ngsi_v2/subscriptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/filip/models/ngsi_v2/subscriptions.py b/filip/models/ngsi_v2/subscriptions.py index 7aa87c1c..ed6af411 100644 --- a/filip/models/ngsi_v2/subscriptions.py +++ b/filip/models/ngsi_v2/subscriptions.py @@ -413,7 +413,8 @@ def check_alteration_types(cls, v): elif isinstance(v, list): for item in v: if item not in allowed_types: - raise ValueError(f'{item} is not a valid alterationType') + raise ValueError(f'{item} is not a valid alterationType' + f' allowed values are {allowed_types}') return v else: raise ValueError('alterationTypes must be a list of strings')