Skip to content

Update entity with property value = 0 in update_entity_attribute #450

@hannahgoerigk

Description

@hannahgoerigk

Describe the bug
When updating an entity in FIWARE with e.g. {"temperature": 0.0} the update is not done as the attribute model dump only considers "true" values:

prop = attr.model_dump()
            for key, value in prop.items():
                if value and value != "Property":
                    jsonnn[key] = value

To Reproduce
This is a minimal example producing the unexpected behaviour. I copied the update_entity_attribute function from the cb.py and adapted it minimally so it would work as a standalone.

import logging
import requests
from typing import Union
from urllib.parse import urljoin

from filip.clients.ngsi_ld.cb import ContextBrokerLDClient
from filip.models.ngsi_ld.context import ContextProperty, ContextRelationship, ContextLDEntity, NamedContextRelationship, NamedContextProperty

BASE_URL =<your-url>
ENTITY_ID = f"urn:ngsi-ld:Test:test001"
ENTITY_TYPE = "Test"
ATTR_NAME = "temperature"


class MinimalFiwareService:

    def __init__(self, base_url: str):
        self.base_url = base_url
        self._url_version = "ngsi-ld/v1"
       # if needed add session
        self.cb_client = ContextBrokerLDClient(url=base_url)

    def post_entity(self, entity: ContextLDEntity):
        self.cb_client.post_entity(entity=entity)

    def get_entity_attribute(self, entity_id: str, attr_name: str) -> dict:
        return self.cb_client.get_entity(entity_id=entity_id, attrs=[attr_name], options='keyValues')

    def delete_entity(self, entity_id: str):
        self.cb_client.delete_entity_by_id(entity_id=entity_id)

    def update_entity_attribute_base(
            self,
            entity_id: str,
            attr: Union[
                ContextProperty,
                ContextRelationship,
                NamedContextProperty,
                NamedContextRelationship,
            ],
            attr_name: str = None,
    ):
        headers = self.cb_client.headers.copy()

        url = urljoin(
            self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}"
        )

        jsonnn = {}
        if isinstance(attr, list) or isinstance(attr, NamedContextProperty):
            jsonnn = attr.model_dump(exclude={"name"}, exclude_none=True)
        else:
            prop = attr.model_dump()
            for key, value in prop.items():
                ################
                #### ISSUE #####
                ################
                # If value is 0, it will not be included in the payload ###
                if value and value != "Property":
                    jsonnn[key] = value

        try:
            res = self.cb_client.patch(url=url, headers=headers, json=jsonnn)
            if res.ok:
                pass
            else:
                print(f"Response: {res.status_code}, {res.text}\n Attribute properties: {prop}")
        except requests.RequestException as err:
            msg = f"Could not update attribute '{attr_name}' of entity {entity_id}"
            print(msg)

    def update_entity_attribute_adapted(
            self,
            entity_id: str,
            attr: Union[
                ContextProperty,
                ContextRelationship,
                NamedContextProperty,
                NamedContextRelationship,
            ],
            attr_name: str = None,
    ):
        headers = self.cb_client.headers.copy()

        url = urljoin(
            self.base_url, f"{self._url_version}/entities/{entity_id}/attrs/{attr_name}"
        )

        jsonnn = {}
        if isinstance(attr, list) or isinstance(attr, NamedContextProperty):
            jsonnn = attr.model_dump(exclude={"name"}, exclude_none=True)
        else:
            prop = attr.model_dump()
            for key, value in prop.items():
                #################
                #### CHANGE #####
                #################
                if value is not None and value != "Property":
                    jsonnn[key] = value

        try:
            res = self.cb_client.patch(url=url, headers=headers, json=jsonnn)
            if res.ok:
                pass
            else:
                res.raise_for_status()
        except requests.RequestException as err:
            msg = f"Could not update attribute '{attr_name}' of entity {entity_id}.\n{err}"
            print(err)


if __name__ == "__main__":
    service = MinimalFiwareService(base_url=BASE_URL)

    initial_attributes = {
        ATTR_NAME: ContextProperty(value=99.9)
    }
    test_entity = ContextLDEntity(id=ENTITY_ID, type=ENTITY_TYPE, **initial_attributes)

    try:
        service.post_entity(test_entity)

        initial_state = service.get_entity_attribute(ENTITY_ID, ATTR_NAME)
        print(f"Initial State:\n{initial_state}")

        print("\nATTEMPTING UPDATE WITH BASE FUNCTION")

        attribute_to_update = {
            ATTR_NAME: ContextProperty(value=0.0)
        }

        attr_name, attr_value = list(attribute_to_update.items())[0]

        try:
            service.update_entity_attribute_base(
                entity_id=ENTITY_ID,
                attr=attr_value,
                attr_name=ATTR_NAME
            )
        except requests.RequestException as err:
            print(f"The base function threw an error: {err, err.args}")

        state_after_base_update = service.get_entity_attribute(ENTITY_ID, ATTR_NAME)
        print(f"\nState after update (base function):\n{state_after_base_update}")


        print("\nATTEMPTING UPDATE WITH ADAPTED FUNCTION")
        try:
            service.update_entity_attribute_adapted(
                entity_id=ENTITY_ID,
                attr=attr_value,
                attr_name=ATTR_NAME
            )
            state_after_adapted_update = service.get_entity_attribute(ENTITY_ID, ATTR_NAME)
            print(f"State after update (adapted function):\n{state_after_adapted_update}")
        except Exception as e:
            print(f"The adapted function threw an unexpected error: {e}")


    finally:
        print("\nCLEANUP")
        service.delete_entity(entity_id=ENTITY_ID)
        print(f"Test entity {ENTITY_ID} deleted.")

Expected behavior
Expected output shows, that the base function can not handle values of 0, as they equal false.

Initial State:
id='urn:ngsi-ld:Test:test001' type='Test' @context=['https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.6.jsonld', 'https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.8.jsonld'] temperature=99.9

ATTEMPTING UPDATE WITH BASE FUNCTION
Response: 400, {"type":"https://uri.etsi.org/ngsi-ld/errors/BadRequestData","title":"Invalid Payload Body","detail":"Empty Object"}
 Attribute properties: {'type': 'Property', 'value': 0.0, 'observedAt': None, 'createdAt': None, 'modifiedAt': None, 'UnitCode': None, 'datasetId': None}

State after update (base function):
id='urn:ngsi-ld:Test:test001' type='Test' @context=['https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.6.jsonld', 'https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.8.jsonld'] temperature=99.9

ATTEMPTING UPDATE WITH ADAPTED FUNCTION
State after update (adapted function):
id='urn:ngsi-ld:Test:test001' type='Test' @context=['https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.6.jsonld', 'https://uri.etsi.org/ngsi-ld/v1/ngsi-ld-core-context-v1.8.jsonld'] temperature=0

CLEANUP
Test entity urn:ngsi-ld:Test:test001 deleted.

Additional context
I am not sure if this is expected behaviour or a bug. If expected, you can close the issue.

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions