diff --git a/odata/context.py b/odata/context.py index 6cc8441..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): + 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,19 +59,21 @@ def save(self, entity, force_refresh=True, extra_headers=None): :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 """ 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: Union[bool, list[str]] = False): """ Creates a POST call to the service, sending the complete new entity @@ -84,7 +87,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 +98,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: Union[bool, list[str]] = False): """ Creates a PATCH call to the service, sending only the modified values @@ -106,7 +109,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..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,14 +244,16 @@ 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: 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 """ - 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..37aa414 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): - return self._clean_new_entity(self.entity) + 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): + 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,9 +211,23 @@ 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): + 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 (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: + # 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__ @@ -259,7 +273,15 @@ 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) - return insert_data + return EntityState._filter_null_properties(insert_data, omit_null_props)