diff --git a/CHANGELOG.md b/CHANGELOG.md index 33f33802..981a82fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +### Added +- Specify PATCH or PUT method for EntityUpdateRequest - Barton Ip +- <, <=, >, >= operators on GetEntitySetFilter - Barton Ip + ### Fixed - URL encode $filter contents - Barton Ip +- Headers attribute on ODataHttpRequest - Barton Ip ## [1.5.0] diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index abb4022c..e703ff70 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -239,7 +239,7 @@ def __init__(self, url, connection, handler, headers=None): self._connection = connection self._url = url self._handler = handler - self._headers = headers + self._headers = headers or dict() self._logger = logging.getLogger(LOGGER_NAME) @property @@ -272,6 +272,12 @@ def get_headers(self): # pylint: disable=no-self-use return None + def add_headers(self, value): + if not isinstance(value, dict): + raise TypeError("Headers must be of type 'dict' not {}".format(type(value))) + + self._headers.update(value) + def execute(self): """Fetches HTTP response and returns processed result @@ -284,7 +290,7 @@ def execute(self): # pylint: disable=assignment-from-none body = self.get_body() - headers = {} if self._headers is None else self._headers + headers = self._headers # pylint: disable=assignment-from-none extra_headers = self.get_headers() @@ -351,7 +357,7 @@ def get_path(self): return self._entity_set_proxy.last_segment + self._entity_key.to_key_string() def get_headers(self): - return {'Accept': 'application/json'} + return {'Accept': 'application/json', **self._headers} def get_query_params(self): qparams = super(EntityGetRequest, self).get_query_params() @@ -448,7 +454,7 @@ def get_body(self): return json.dumps(self._get_body()) def get_headers(self): - return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X'} + return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X', **self._headers} @staticmethod def _build_values(entity_type, entity): @@ -512,13 +518,18 @@ class EntityModifyRequest(ODataHttpRequest): Call execute() to send the update-request to the OData service and get the modified entity.""" - def __init__(self, url, connection, handler, entity_set, entity_key): + def __init__(self, url, connection, handler, entity_set, entity_key, method="PATCH"): super(EntityModifyRequest, self).__init__(url, connection, handler) self._logger = logging.getLogger(LOGGER_NAME) self._entity_set = entity_set self._entity_type = entity_set.entity_type self._entity_key = entity_key + if method.upper() not in ["PATCH", "PUT"]: + raise ValueError("Method must be either PATCH or PUT") + + self._method = method + self._values = {} # get all properties declared by entity type @@ -531,7 +542,7 @@ def get_path(self): def get_method(self): # pylint: disable=no-self-use - return 'PATCH' + return self._method def get_body(self): # pylint: disable=no-self-use @@ -541,7 +552,7 @@ def get_body(self): return json.dumps(body) def get_headers(self): - return {'Accept': 'application/json', 'Content-Type': 'application/json'} + return {'Accept': 'application/json', 'Content-Type': 'application/json', **self._headers} def set(self, **kwargs): """Set properties to be changed.""" @@ -639,6 +650,7 @@ def get_headers(self): return { 'Accept': 'application/json', + **self._headers } def get_query_params(self): @@ -699,6 +711,7 @@ def get_method(self): def get_headers(self): return { 'Accept': 'application/json', + **self._headers } @@ -956,6 +969,18 @@ def __eq__(self, value): def __ne__(self, value): return GetEntitySetFilter.format_filter(self._proprty, 'ne', value) + def __lt__(self, value): + return GetEntitySetFilter.format_filter(self._proprty, 'lt', value) + + def __le__(self, value): + return GetEntitySetFilter.format_filter(self._proprty, 'le', value) + + def __ge__(self, value): + return GetEntitySetFilter.format_filter(self._proprty, 'ge', value) + + def __gt__(self, value): + return GetEntitySetFilter.format_filter(self._proprty, 'gt', value) + class GetEntitySetRequest(QueryRequest): """GET on EntitySet""" @@ -1140,7 +1165,7 @@ def create_entity_handler(response): return EntityCreateRequest(self._service.url, self._service.connection, create_entity_handler, self._entity_set, self.last_segment) - def update_entity(self, key=None, **kwargs): + def update_entity(self, key=None, method="PATCH", **kwargs): """Updates an existing entity in the given entity-set.""" def update_entity_handler(response): @@ -1158,7 +1183,7 @@ def update_entity_handler(response): self._logger.info('Updating entity %s for key %s and args %s', self._entity_set.entity_type.name, key, kwargs) return EntityModifyRequest(self._service.url, self._service.connection, update_entity_handler, self._entity_set, - entity_key) + entity_key, method=method) def delete_entity(self, key: EntityKey = None, **kwargs): """Delete the entity""" @@ -1433,7 +1458,7 @@ def get_boundary(self): def get_headers(self): # pylint: disable=no-self-use - return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary())} + return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary()), **self._headers} def get_body(self): return encode_multipart(self.get_boundary(), self.requests) diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py index 622952dd..cc0e1b21 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -658,6 +658,66 @@ def test_update_entity_with_entity_key(service): assert query.get_path() == "TemperatureMeasurements(Sensor='sensor1',Date=datetime'2017-12-24T18:00:00')" +def test_update_entity_with_put_method_specified(service): + """Make sure the method update_entity handles correctly when PUT method is specified""" + + # pylint: disable=redefined-outer-name + + + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + query = service.entity_sets.TemperatureMeasurements.update_entity(key, method="PUT") + assert query.get_method() == "PUT" + + +def test_update_entity_with_patch_method_specified(service): + """Make sure the method update_entity handles correctly when PATCH method is specified""" + + # pylint: disable=redefined-outer-name + + + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + query = service.entity_sets.TemperatureMeasurements.update_entity(key, method="PATCH") + assert query.get_method() == "PATCH" + + +def test_update_entity_with_no_method_specified(service): + """Make sure the method update_entity handles correctly when no method is specified""" + + # pylint: disable=redefined-outer-name + + + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + query = service.entity_sets.TemperatureMeasurements.update_entity(key) + assert query.get_method() == "PATCH" + + +def test_update_entity_with_wrong_method_specified(service): + """Make sure the method update_entity raises ValueError when wrong method is specified""" + + # pylint: disable=redefined-outer-name + + + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + with pytest.raises(ValueError): + service.entity_sets.TemperatureMeasurements.update_entity(key, method="DELETE") + + def test_get_entity_with_entity_key_and_other_params(service): """Make sure the method update_entity handles correctly the parameter key which is EntityKey""" @@ -671,6 +731,67 @@ def test_get_entity_with_entity_key_and_other_params(service): query = service.entity_sets.TemperatureMeasurements.update_entity(key=key, Foo='Bar') assert query.get_path() == "TemperatureMeasurements(Sensor='sensor1',Date=datetime'2017-12-24T18:00:00')" + +def test_get_entities_with_custom_headers(service): + query = service.entity_sets.TemperatureMeasurements.get_entities() + query.add_headers({"X-Foo": "bar"}) + + assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"} + + +def test_get_entity_with_custom_headers(service): + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + query = service.entity_sets.TemperatureMeasurements.get_entity(key) + query.add_headers({"X-Foo": "bar"}) + + assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"} + + +def test_update_entities_with_custom_headers(service): + key = EntityKey( + service.schema.entity_type('TemperatureMeasurement'), + Sensor='sensor1', + Date=datetime.datetime(2017, 12, 24, 18, 0)) + + query = service.entity_sets.TemperatureMeasurements.update_entity(key) + query.add_headers({"X-Foo": "bar"}) + + assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Foo": "bar"} + + +def test_create_entity_with_custom_headers(service): + query = service.entity_sets.TemperatureMeasurements.create_entity() + query.add_headers({"X-Foo": "bar"}) + + assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "X", "X-Foo": "bar"} + + +def test_create_entity_with_overwriting_custom_headers(service): + query = service.entity_sets.TemperatureMeasurements.create_entity() + query.add_headers({"X-Requested-With": "bar"}) + + assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "bar"} + + +def test_create_entity_with_blank_custom_headers(service): + query = service.entity_sets.TemperatureMeasurements.create_entity() + query.add_headers({}) + + assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "X"} + + +def test_pass_incorrect_header_type(service): + query = service.entity_sets.TemperatureMeasurements.create_entity() + + with pytest.raises(TypeError) as ex: + query.add_headers(69420) + assert str(ex) == "TypeError: Headers must be of type 'dict' not " + + @responses.activate def test_get_entities(service): """Get entities""" @@ -1332,6 +1453,50 @@ def test_get_entity_set_query_filter_ne(service): assert filter_str == "Key ne 'bar'" +def test_get_entity_set_query_filter_lt(service): + """Test the operator 'lt' of $filter for humans""" + + # pylint: disable=redefined-outer-name, invalid-name + + request = service.entity_sets.Cars.get_entities() + filter_str = request.Price < 2 + + assert filter_str == "Price lt 2" + + +def test_get_entity_set_query_filter_le(service): + """Test the operator 'le' of $filter for humans""" + + # pylint: disable=redefined-outer-name, invalid-name + + request = service.entity_sets.Cars.get_entities() + filter_str = request.Price <= 2 + + assert filter_str == "Price le 2" + + +def test_get_entity_set_query_filter_ge(service): + """Test the operator 'ge' of $filter for humans""" + + # pylint: disable=redefined-outer-name, invalid-name + + request = service.entity_sets.Cars.get_entities() + filter_str = request.Price >= 2 + + assert filter_str == "Price ge 2" + + +def test_get_entity_set_query_filter_gt(service): + """Test the operator 'gt' of $filter for humans""" + + # pylint: disable=redefined-outer-name, invalid-name + + request = service.entity_sets.Cars.get_entities() + filter_str = request.Price > 2 + + assert filter_str == "Price gt 2" + + def test_get_entity_set_query_filter_and(service): """Test the operator 'and' of $filter for humans"""