From 5a5deff12838a29e3388b45826f3dfc33b31595f Mon Sep 17 00:00:00 2001 From: Jonathan Moyes Date: Wed, 9 Oct 2024 16:46:46 -0600 Subject: [PATCH 1/4] save(): set omit_null_props=True, or list of keys to omit from request --- odata/context.py | 14 +++++++------- odata/service.py | 4 ++-- odata/state.py | 33 ++++++++++++++++++++++++++------- 3 files changed, 35 insertions(+), 16 deletions(-) diff --git a/odata/context.py b/odata/context.py index 6cc8441..9ec5d1b 100644 --- a/odata/context.py +++ b/odata/context.py @@ -49,7 +49,7 @@ def delete(self, entity): entity.__odata__.persisted = False self.log.info(u'Success') - def save(self, entity, force_refresh=True, extra_headers=None): + def save(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[]): """ Creates a POST or PATCH call to the service. If the entity already has a primary key, an update is called. Otherwise the entity is inserted @@ -63,14 +63,14 @@ def save(self, entity, force_refresh=True, extra_headers=None): """ if self.is_entity_saved(entity): - self._update_existing(entity, force_refresh=force_refresh, extra_headers=extra_headers) + self._update_existing(entity, force_refresh=force_refresh, extra_headers=extra_headers, omit_null_props=omit_null_props) else: - self._insert_new(entity) + self._insert_new(entity, omit_null_props=omit_null_props) def is_entity_saved(self, entity): return entity.__odata__.persisted - def _insert_new(self, entity): + def _insert_new(self, entity, omit_null_props=[]): """ Creates a POST call to the service, sending the complete new entity @@ -84,7 +84,7 @@ def _insert_new(self, entity): self.log.info(u'Saving new entity') es = entity.__odata__ - insert_data = es.data_for_insert() + insert_data = es.data_for_insert(omit_null_props) saved_data = self.connection.execute_post(url, insert_data) es.reset() es.connection = self.connection @@ -95,7 +95,7 @@ def _insert_new(self, entity): self.log.info(u'Success') - def _update_existing(self, entity, force_refresh=True, extra_headers=None): + def _update_existing(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[]): """ Creates a PATCH call to the service, sending only the modified values @@ -106,7 +106,7 @@ def _update_existing(self, entity, force_refresh=True, extra_headers=None): msg = 'Cannot update Entity that does not belong to EntitySet: {0}'.format(entity) raise ODataError(msg) - patch_data = es.data_for_update() + patch_data = es.data_for_update(omit_null_props) if len([i for i in patch_data if not i.startswith('@')]) == 0: self.log.debug(u'Nothing to update: {0}'.format(entity)) diff --git a/odata/service.py b/odata/service.py index 9c2fb0a..e94b53a 100644 --- a/odata/service.py +++ b/odata/service.py @@ -244,7 +244,7 @@ def delete(self, entity): """ return self.default_context.delete(entity) - def save(self, entity, force_refresh=True): + def save(self, entity, force_refresh=True, omit_null_props=[]): """ Creates a POST or PATCH call to the service. If the entity already has a primary key, an update is called. Otherwise the entity is inserted @@ -254,4 +254,4 @@ def save(self, entity, force_refresh=True): :param force_refresh: Read full entity data again from service after PATCH call :raises ODataConnectionError: Invalid data or serverside error. Server returned an HTTP error code """ - return self.default_context.save(entity, force_refresh=force_refresh) + return self.default_context.save(entity, force_refresh=force_refresh, omit_null_props=omit_null_props) diff --git a/odata/state.py b/odata/state.py index 2162f0e..bf02748 100644 --- a/odata/state.py +++ b/odata/state.py @@ -188,10 +188,10 @@ def set_property_dirty(self, prop): if prop.name not in self.dirty: self.dirty.append(prop.name) - def data_for_insert(self): - return self._clean_new_entity(self.entity) + def data_for_insert(self, omit_null_props=[]): + return self._clean_new_entity(self.entity, omit_null_props) - def data_for_update(self): + def data_for_update(self, omit_null_props=[]): update_data = OrderedDict() update_data['@odata.type'] = self.entity.__odata_type__ @@ -211,9 +211,19 @@ def data_for_update(self): update_data[key] = [i.__odata__.id for i in value] else: update_data[key] = value.__odata__.id - return update_data - - def _clean_new_entity(self, entity): + + update_data_filtered = {} + for prop_name, prop in update_data.items(): + if omit_null_props is True or prop_name in omit_null_props: + # Should omit unless not None + if prop is None: + # Omit from request + continue + update_data_filtered[prop_name] = prop + + return update_data_filtered + + def _clean_new_entity(self, entity, omit_null_props=[]): """:type entity: odata.entity.EntityBase """ insert_data = OrderedDict() insert_data['@odata.type'] = entity.__odata_type__ @@ -262,4 +272,13 @@ def _clean_new_entity(self, entity): else: insert_data[prop.name] = self._clean_new_entity(value) - return insert_data + insert_data_filtered = {} + for prop_name, prop in insert_data.items(): + if omit_null_props is True or prop_name in omit_null_props: + # Should omit unless not None + if prop is None: + # Omit from request + continue + insert_data_filtered[prop_name] = prop + + return insert_data_filtered From 9f89d920a10ed443e1871a6232ba1655219bf617 Mon Sep 17 00:00:00 2001 From: Jonathan Moyes Date: Wed, 9 Oct 2024 16:58:13 -0600 Subject: [PATCH 2/4] Support foreign key props to service, without service support for @odata.bind --- odata/state.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/odata/state.py b/odata/state.py index 2162f0e..45ec04f 100644 --- a/odata/state.py +++ b/odata/state.py @@ -259,6 +259,14 @@ def _clean_new_entity(self, entity): else: if value.__odata__.id: insert_data['{0}@odata.bind'.format(prop.name)] = value.__odata__.id + + # Put the foreign key back into the request for compatibility with + # systems that don't handle {entity} odata.bind correctly + try: + insert_data[prop.foreign_key] = getattr(value, prop.foreign_key) + except: + pass + else: insert_data[prop.name] = self._clean_new_entity(value) From 460ba3a5e0ae1f66fb7a37aded97f26d05ec8608 Mon Sep 17 00:00:00 2001 From: Cristian Libotean Date: Thu, 10 Oct 2024 09:00:48 +0300 Subject: [PATCH 3/4] Added type options for omit_null_props and made default False. --- odata/context.py | 9 ++++++--- odata/service.py | 6 ++++-- odata/state.py | 47 +++++++++++++++++++++-------------------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/odata/context.py b/odata/context.py index 9ec5d1b..60d4d81 100644 --- a/odata/context.py +++ b/odata/context.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import logging +from typing import Union from odata.query import Query from odata.connection import ODataConnection @@ -49,7 +50,7 @@ def delete(self, entity): entity.__odata__.persisted = False self.log.info(u'Success') - def save(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[]): + def save(self, entity, force_refresh: bool = True, extra_headers=None, omit_null_props: Union[bool, list[str]] = False): """ Creates a POST or PATCH call to the service. If the entity already has a primary key, an update is called. Otherwise the entity is inserted @@ -58,6 +59,8 @@ def save(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[ :param entity: Model instance to insert or update :type entity: EntityBase :param force_refresh: Read full entity data again from service after PATCH call + :param omit_null_props: True/False or a list of properties to omit. + If set to true or given a list of properties these will be ommited from the request if they're set to None/null :param extra_headers: Add custom headers on patch, post (Example:B1S-ReplaceCollectionsOnPatch=true) :raises ODataConnectionError: Invalid data or serverside error. Server returned an HTTP error code """ @@ -70,7 +73,7 @@ def save(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[ def is_entity_saved(self, entity): return entity.__odata__.persisted - def _insert_new(self, entity, omit_null_props=[]): + def _insert_new(self, entity, omit_null_props: Union[bool, list[str]] = False): """ Creates a POST call to the service, sending the complete new entity @@ -95,7 +98,7 @@ def _insert_new(self, entity, omit_null_props=[]): self.log.info(u'Success') - def _update_existing(self, entity, force_refresh=True, extra_headers=None, omit_null_props=[]): + def _update_existing(self, entity, force_refresh=True, extra_headers=None, omit_null_props: Union[bool, list[str]] = False): """ Creates a PATCH call to the service, sending only the modified values diff --git a/odata/service.py b/odata/service.py index e94b53a..ad881b9 100644 --- a/odata/service.py +++ b/odata/service.py @@ -56,7 +56,7 @@ import logging import sys import urllib.parse -from typing import Optional, TypeVar +from typing import Optional, TypeVar, Union import rich import rich.console @@ -244,13 +244,15 @@ def delete(self, entity): """ return self.default_context.delete(entity) - def save(self, entity, force_refresh=True, omit_null_props=[]): + def save(self, entity, force_refresh=True, omit_null_props: Union[bool, list[str]] = False): """ Creates a POST or PATCH call to the service. If the entity already has a primary key, an update is called. Otherwise the entity is inserted as new. Updating an entity will only send the changed values :param entity: Model instance to insert or update + :param omit_null_props: True/False or a list of properties to omit. + If set to true or given a list of properties these will be ommited from the request if they're set to None/null :param force_refresh: Read full entity data again from service after PATCH call :raises ODataConnectionError: Invalid data or serverside error. Server returned an HTTP error code """ diff --git a/odata/state.py b/odata/state.py index 44bb642..84f148e 100644 --- a/odata/state.py +++ b/odata/state.py @@ -5,7 +5,7 @@ import inspect import itertools from collections import OrderedDict -from typing import Optional +from typing import Optional, Union import rich import rich.panel @@ -188,10 +188,10 @@ def set_property_dirty(self, prop): if prop.name not in self.dirty: self.dirty.append(prop.name) - def data_for_insert(self, omit_null_props=[]): + def data_for_insert(self, omit_null_props: Union[bool, list[str]] = False): return self._clean_new_entity(self.entity, omit_null_props) - def data_for_update(self, omit_null_props=[]): + def data_for_update(self, omit_null_props: Union[bool, list[str]] = False): update_data = OrderedDict() update_data['@odata.type'] = self.entity.__odata_type__ @@ -211,19 +211,23 @@ def data_for_update(self, omit_null_props=[]): update_data[key] = [i.__odata__.id for i in value] else: update_data[key] = value.__odata__.id - - update_data_filtered = {} - for prop_name, prop in update_data.items(): - if omit_null_props is True or prop_name in omit_null_props: - # Should omit unless not None - if prop is None: - # Omit from request - continue - update_data_filtered[prop_name] = prop - - return update_data_filtered - - def _clean_new_entity(self, entity, omit_null_props=[]): + + return EntityState._filter_null_properties(update_data, omit_null_props) + + @staticmethod + def _filter_null_properties(properties: dict, omit_null_props: Union[bool, list[str]]) -> dict: + if omit_null_props is True or len(omit_null_props) > 0: + filtered_properties = {} + for prop_name, prop in properties.items(): + if omit_null_props is True or prop_name in omit_null_props: + # Should omit unless not None + if prop is not None: + filtered_properties[prop_name] = prop + return filtered_properties + else: + return properties + + def _clean_new_entity(self, entity, omit_null_props: Union[str, list[str]] = False): """:type entity: odata.entity.EntityBase """ insert_data = OrderedDict() insert_data['@odata.type'] = entity.__odata_type__ @@ -280,13 +284,4 @@ def _clean_new_entity(self, entity, omit_null_props=[]): else: insert_data[prop.name] = self._clean_new_entity(value) - insert_data_filtered = {} - for prop_name, prop in insert_data.items(): - if omit_null_props is True or prop_name in omit_null_props: - # Should omit unless not None - if prop is None: - # Omit from request - continue - insert_data_filtered[prop_name] = prop - - return insert_data_filtered + return EntityState._filter_null_properties(insert_data, omit_null_props) From 78b957ed8d7aad87245f5912504437a0574323e9 Mon Sep 17 00:00:00 2001 From: Jonathan Moyes Date: Thu, 10 Oct 2024 14:09:15 -0600 Subject: [PATCH 4/4] Check if omit_null_props is False: don't call len() on a bool --- odata/state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/odata/state.py b/odata/state.py index 84f148e..37aa414 100644 --- a/odata/state.py +++ b/odata/state.py @@ -216,7 +216,7 @@ def data_for_update(self, omit_null_props: Union[bool, list[str]] = False): @staticmethod def _filter_null_properties(properties: dict, omit_null_props: Union[bool, list[str]]) -> dict: - if omit_null_props is True or len(omit_null_props) > 0: + if omit_null_props is True or (type(omit_null_props) == list and len(omit_null_props) > 0): filtered_properties = {} for prop_name, prop in properties.items(): if omit_null_props is True or prop_name in omit_null_props: