From 866db9f9cc04ed21ad715e0fb445482068126699 Mon Sep 17 00:00:00 2001 From: bartonip Date: Fri, 26 Jun 2020 04:49:52 +0000 Subject: [PATCH 01/10] Fixed filter not working for services that require URLEncoded filters --- pyodata/v2/service.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 950173e8..abb4022c 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -14,6 +14,13 @@ from http.client import HTTPResponse from io import BytesIO +try: + # For Python 3.0 and later + from urllib.parse import quote +except ImportError: + # Fallback to urllib2 + from urllib2 import quote + from pyodata.exceptions import HttpError, PyODataException, ExpressionError from . import model @@ -592,7 +599,7 @@ def expand(self, expand): def filter(self, filter_val): """Sets the filter expression.""" # returns QueryRequest - self._filter = filter_val + self._filter = quote(filter_val) return self # def nav(self, key_value, nav_property): From 87cfac9815782e03466387edf0d137e02a148685 Mon Sep 17 00:00:00 2001 From: bartonip Date: Fri, 26 Jun 2020 10:49:54 +0000 Subject: [PATCH 02/10] Added additional operators to GetEntitySetFilter --- pyodata/v2/service.py | 12 +++++++++++ tests/test_service_v2.py | 44 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index abb4022c..b22cf0c7 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -956,6 +956,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""" diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py index 622952dd..699fbd8a 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -1332,6 +1332,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""" From 06a52b8f4a4ee131c57f6424c0fe2f09b22b35da Mon Sep 17 00:00:00 2001 From: bartonip Date: Fri, 26 Jun 2020 11:23:53 +0000 Subject: [PATCH 03/10] Made headers attribute actually affect the headers --- pyodata/v2/service.py | 16 ++++++------ tests/test_service_v2.py | 53 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index b22cf0c7..7239d738 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -235,11 +235,11 @@ def __repr__(self): class ODataHttpRequest: """Deferred HTTP Request""" - def __init__(self, url, connection, handler, headers=None): + def __init__(self, url, connection, handler, headers={}): self._connection = connection self._url = url self._handler = handler - self._headers = headers + self.headers = headers self._logger = logging.getLogger(LOGGER_NAME) @property @@ -284,7 +284,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 +351,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 +448,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): @@ -541,7 +541,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 +639,7 @@ def get_headers(self): return { 'Accept': 'application/json', + **self.headers } def get_query_params(self): @@ -699,6 +700,7 @@ def get_method(self): def get_headers(self): return { 'Accept': 'application/json', + **self.headers } @@ -1445,7 +1447,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 699fbd8a..5c535198 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -671,6 +671,59 @@ 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.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.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.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.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.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.headers = {} + + assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "X"} + + @responses.activate def test_get_entities(service): """Get entities""" From 95ad17f0d3150670c3f93ae800eca37465eaf7ef Mon Sep 17 00:00:00 2001 From: bartonip Date: Fri, 26 Jun 2020 11:47:07 +0000 Subject: [PATCH 04/10] Allow method to be specified in EntityModifyRequest --- pyodata/v2/service.py | 13 ++++++--- tests/test_service_v2.py | 60 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 7239d738..556b3511 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -512,13 +512,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 +536,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 @@ -1154,7 +1159,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): @@ -1172,7 +1177,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""" diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py index 5c535198..3caf3e61 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""" From 3f98d274f1be386a697c2d54b4a020b192a513a0 Mon Sep 17 00:00:00 2001 From: Barton Ip Date: Fri, 26 Jun 2020 21:56:26 +1000 Subject: [PATCH 05/10] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) 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] From b6351d4908265b4667f6b733278e58cc3ddec023 Mon Sep 17 00:00:00 2001 From: bartonip Date: Sun, 28 Jun 2020 03:00:52 +0000 Subject: [PATCH 06/10] Fixed custom headers implementation as per #111 --- pyodata/v2/service.py | 24 +++++++++++++++--------- tests/test_service_v2.py | 20 ++++++++++++++------ 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 556b3511..e703ff70 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -235,11 +235,11 @@ def __repr__(self): class ODataHttpRequest: """Deferred HTTP Request""" - def __init__(self, url, connection, handler, headers={}): + 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 = 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', **self.headers} + 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', **self.headers} + return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X', **self._headers} @staticmethod def _build_values(entity_type, entity): @@ -546,7 +552,7 @@ def get_body(self): return json.dumps(body) def get_headers(self): - return {'Accept': 'application/json', 'Content-Type': 'application/json', **self.headers} + return {'Accept': 'application/json', 'Content-Type': 'application/json', **self._headers} def set(self, **kwargs): """Set properties to be changed.""" @@ -644,7 +650,7 @@ def get_headers(self): return { 'Accept': 'application/json', - **self.headers + **self._headers } def get_query_params(self): @@ -705,7 +711,7 @@ def get_method(self): def get_headers(self): return { 'Accept': 'application/json', - **self.headers + **self._headers } @@ -1452,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()), **self.headers} + 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 3caf3e61..cc0e1b21 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -734,7 +734,7 @@ def test_get_entity_with_entity_key_and_other_params(service): def test_get_entities_with_custom_headers(service): query = service.entity_sets.TemperatureMeasurements.get_entities() - query.headers = {"X-Foo": "bar"} + query.add_headers({"X-Foo": "bar"}) assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"} @@ -746,7 +746,7 @@ def test_get_entity_with_custom_headers(service): Date=datetime.datetime(2017, 12, 24, 18, 0)) query = service.entity_sets.TemperatureMeasurements.get_entity(key) - query.headers = {"X-Foo": "bar"} + query.add_headers({"X-Foo": "bar"}) assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"} @@ -758,32 +758,40 @@ def test_update_entities_with_custom_headers(service): Date=datetime.datetime(2017, 12, 24, 18, 0)) query = service.entity_sets.TemperatureMeasurements.update_entity(key) - query.headers = {"X-Foo": "bar"} + 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.headers = {"X-Foo": "bar"} + 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.headers = {"X-Requested-With": "bar"} + 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.headers = {} + 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""" From 6fe993de74950f1cb5e6ab9f0f07b9f72fe9e433 Mon Sep 17 00:00:00 2001 From: bartonip Date: Sun, 28 Jun 2020 06:24:11 +0000 Subject: [PATCH 07/10] Implemented Django style filtering --- docs/usage/querying.rst | 30 ++++ pyodata/v2/service.py | 156 +++++++++++++++++- tests/test_service_v2.py | 348 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 529 insertions(+), 5 deletions(-) diff --git a/docs/usage/querying.rst b/docs/usage/querying.rst index 0b825946..0e1838f6 100644 --- a/docs/usage/querying.rst +++ b/docs/usage/querying.rst @@ -66,6 +66,36 @@ Print unique identification (Id) of all employees with name John Smith: print(smith.EmployeeID) +Get entities matching a filter in ORM style +--------------------------------------------------- + +Print unique identification (Id) of all employees with name John Smith: + +.. code-block:: python + + from pyodata.v2.service import GetEntitySetFilter as esf + + smith_employees_request = northwind.entity_sets.Employees.get_entities() + smith_employees_request = smith_employees_request.filter(FirstName="John", LastName="Smith") + for smith in smith_employees_request.execute(): + print(smith.EmployeeID) + + +Get entities matching a complex filter in ORM style +--------------------------------------------------- + +Print unique identification (Id) of all employees with name John Smith: + +.. code-block:: python + + from pyodata.v2.service import GetEntitySetFilter as esf + + smith_employees_request = northwind.entity_sets.Employees.get_entities() + smith_employees_request = smith_employees_request.filter(FirstName__contains="oh", LastName__startswith="Smi") + for smith in smith_employees_request.execute(): + print(smith.EmployeeID) + + Get a count of entities ----------------------- diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 71502f7a..26f79b3c 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -303,8 +303,9 @@ def execute(self): if body: self._logger.debug(' body: %s', body) + params = "&".join("%s=%s" % (k,v) for k,v in self.get_query_params().items()) response = self._connection.request( - self.get_method(), url, headers=headers, params=self.get_query_params(), data=body) + self.get_method(), url, headers=headers, params=params, data=body) self._logger.debug('Received response') self._logger.debug(' url: %s', response.url) @@ -612,7 +613,7 @@ def expand(self, expand): def filter(self, filter_val): """Sets the filter expression.""" # returns QueryRequest - self._filter = quote(filter_val) + self._filter = filter_val return self # def nav(self, key_value, nav_property): @@ -982,6 +983,144 @@ def __ge__(self, value): def __gt__(self, value): return GetEntitySetFilter.format_filter(self._proprty, 'gt', value) +class FilterExpression(object): + def __init__(self, *args, **kwargs): + self.expressions = kwargs + self.other = None + self.operator = None + + def __or__(self, other): + self.other = other + self.operator = "or" + return self + + def __and__(self, other): + self.other = other + self.operator = "and" + return self + +class GetEntitySetFilterChainable(object): + """ + Example expressions + FirstName="Tim" + FirstName__contains="Tim" + Age__gt=56 + Age__gte=6 + Age__lt=78 + Age__lte=90 + Age__range=(5,9) + FirstName__in=["Tim", "Bob", "Sam"] + FirstName__startswith="Tim" + FirstName__endswith="mothy" + Addresses__Suburb="Chatswood" + Addresses__Suburb__contains="wood" + """ + operators = ["startswith", "endswith", "lt", "lte", "gt", "gte", "contains", "range", "in", "length"] + def __init__(self, request, filter_expressions, exprs): + self.request = request + self.expressions = exprs + self.filter_expressions = filter_expressions + + def proprty_obj(self, name): + return self.request._entity_type.proprty(name) + + def process_query_objects(self): + filter_expressions = [] + for q in self.filter_expressions: + lhs_expressions = [] + rhs_expressions = [] + for expr, val in q.expressions.items(): + lhs_expressions.append(self.decode_expression(expr, val)) + lhs_expressions = self.combine_expressions(lhs_expressions) + + if q.other: + for expr, val in q.other.expressions.items(): + rhs_expressions.append(self.decode_expression(expr, val)) + rhs_expressions = self.combine_expressions(rhs_expressions) + + filter_expressions.append(f"({lhs_expressions}) {q.operator} ({rhs_expressions})") + else: + filter_expressions.append(lhs_expression) + + return filter_expressions + + def process_expressions(self): + filter_expressions = [] + for expr, val in self.expressions.items(): + filter_expressions.append(self.decode_expression(expr, val)) + + filter_expressions.extend(self.process_query_objects()) + return filter_expressions + + def decode_expression(self, expr, val): + properties = self.request._entity_type._properties.keys() + field = None + # field_heirarchy = [] + operator = "eq" + exprs = expr.split("__") + + for part in exprs: + if part in properties: + field = part + # field_heirarchy.append(part) + elif part in self.__class__.operators: + operator = part + + # field = "/".join(field_heirarchy) + + # target_field = self.proprty_obj(field_heirarchy[-1]) + expression = self.build_expression(field, operator, val) + + return expression + + def combine_expressions(self, expressions): + return " and ".join(expressions) + + def build_expression(self, field_name, operator, value): + target_field = self.proprty_obj(field_name) + if operator not in ["length", "in", "range"]: + value = target_field.to_literal(value) + if operator == "lt": + return f"{field_name} lt {value}" + elif operator == "lte": + return f"{field_name} le {value}" + elif operator == "gte": + return f"{field_name} ge {value}" + elif operator == "gt": + return f"{field_name} gt {value}" + elif operator == "startswith": + return f"startswith({field_name}, {value}) eq true" + elif operator == "endswith": + return f"endswith({field_name}, {value}) eq true" + elif operator == "length": + value = int(value) + return f"length({field_name}) eq {value}" + elif operator in ["contains"]: + return f"substringof({value}, {field_name}) eq true" + elif operator == "range": + if not (isinstance(value, tuple) or isinstance(value, list)): + raise TypeError("Range must be tuple or list not {}".format(type(value))) + if len(value) != 2: + raise ValueError("Only two items can be passed in a range.") + + x = target_field.to_literal(value[0]) + y = target_field.to_literal(value[1]) + return f"{field_name} gte {x} and {field_name} lte {y}" + elif operator == "in": + literal_values = [] + for v in value: + val = target_field.to_literal(v) + literal_values.append(f"{field_name} eq {val}") + return " or ".join(literal_values) + elif operator == "eq": + return f"{field_name} eq {value}" + else: + raise ValueError(f"Invalid expression {operator}") + + def as_filter_string(self): + expressions = self.process_expressions() + result = self.combine_expressions(expressions) + return quote(result) class GetEntitySetRequest(QueryRequest): """GET on EntitySet""" @@ -995,6 +1134,19 @@ def __getattr__(self, name): proprty = self._entity_type.proprty(name) return GetEntitySetFilter(proprty) + def set_filter(self, filter_val): + filter_text = self._filter + " and " if self._filter else "" + filter_text += filter_val + self._filter = filter_text + + def filter(self, *args, **kwargs): + if len(args) and isinstance(args[0], str): + self._filter = args[0] + return self + else: + self.set_filter(GetEntitySetFilterChainable(self, args, kwargs).as_filter_string()) + return self + class EntitySetProxy: """EntitySet Proxy""" diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py index 80f95bf2..152a9b59 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -1640,14 +1640,14 @@ def test_navigation_count(service): @responses.activate -def test_navigation_count_with_filter(service): - """Check getting $count via navigation property with $filter""" +def test_count_with_filter(service): + """Check getting $count with $filter""" # pylint: disable=redefined-outer-name responses.add( responses.GET, - "{0}/Employees(23)/Addresses/$count?%24filter=City%2520eq%2520%2527London%2527".format(service.url), + "{0}/Employees(23)/Addresses/$count?%24filter=City%20eq%20%27London%27".format(service.url), json=3, status=200) @@ -1659,6 +1659,348 @@ def test_navigation_count_with_filter(service): assert request.execute() == 3 +@responses.activate +def test_count_with_chainable_filter(service): + """Check getting $count with $filter and using new filter syntax""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees(23)/Addresses/$count?%24filter=City%20eq%20%27London%27".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entity(23).nav('Addresses').get_entities() + request = employees.filter(City="London").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_lt_operator(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20lt%2023".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__lt=23).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_lte_operator(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20le%2023".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__lte=23).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_gt_operator(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20gt%2023".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__gt=23).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_gte_operator(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20ge%2023".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__gte=23).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_eq_operator(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20eq%2023".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__eq=23).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_in_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=ID%20eq%201%20or%20ID%20eq%202%20or%20ID%20eq%203".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__in=[1,2,3]).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_startswith_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=startswith%28NickName%2C%20%27Tim%27%29%20eq%20true".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(NickName__startswith="Tim").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_endswith_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=endswith%28NickName%2C%20%27othy%27%29%20eq%20true".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(NickName__endswith="othy").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_length_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=length%28NickName%29%20eq%206".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(NickName__length=6).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_length_operator_as_string(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=length%28NickName%29%20eq%206".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(NickName__length="6").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_contains_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=substringof%28%27Tim%27%2C%20NickName%29%20eq%20true".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(NickName__contains="Tim").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_range_operator(service): + """Check getting $count with $filter in""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=ID%20gte%2020%20and%20ID%20lte%2050".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__range=(20, 50)).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_multiple(service): + """Check getting $count with $filter with new filter syntax using multiple filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?%24filter=ID%20eq%2023%20and%20NickName%20eq%20%27Steve%27".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID=23, NickName="Steve").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + +@responses.activate +def test_count_with_chainable_filter_or(service): + """Check getting $count with $filter with FilterExpression syntax or""" + from pyodata.v2.service import FilterExpression as Q + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=%28ID%20eq%2023%20and%20NickName%20eq%20%27Steve%27%29%20or%20%28ID%20eq%2025%20and%20NickName%20eq%20%27Tim%27%29".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(Q(ID=23, NickName="Steve") | Q(ID=25, NickName="Tim")).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + +@responses.activate +def test_count_with_multiple_filters_startswith(service): + """Check getting $count with $filter calling startswith""" + from pyodata.v2.service import FilterExpression as Q + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=%28ID%20eq%2023%20and%20startswith%28NickName%2C%20%27Ste%27%29%20eq%20true%29%20or%20%28ID%20eq%2025%20and%20NickName%20eq%20%27Tim%27%29".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(Q(ID=23, NickName__startswith="Ste") | Q(ID=25, NickName="Tim")).count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + + + + +@responses.activate +def test_count_with_chained_filters(service): + """Check getting $count with chained filters""" + + # pylint: disable=redefined-outer-name + + responses.add( + responses.GET, + "{0}/Employees/$count?$filter=ID%20gte%2020%20and%20ID%20lte%2050%20and%20NickName%20eq%20%27Tim%27".format(service.url), + json=3, + status=200) + + employees = service.entity_sets.Employees.get_entities() + request = employees.filter(ID__range=(20, 50)).filter(NickName="Tim").count() + + assert isinstance(request, pyodata.v2.service.GetEntitySetRequest) + + assert request.execute() == 3 + + @responses.activate def test_create_entity_with_datetime(service): """ From f27eae46dce6c107b6823398da7b399fd60aaab1 Mon Sep 17 00:00:00 2001 From: bartonip Date: Sun, 28 Jun 2020 07:03:49 +0000 Subject: [PATCH 08/10] Fixed linting issues --- pyodata/v2/service.py | 885 +++++++++++++++++++++++++++++------------- 1 file changed, 609 insertions(+), 276 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 26f79b3c..4a215948 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -24,7 +24,7 @@ from pyodata.exceptions import HttpError, PyODataException, ExpressionError from . import model -LOGGER_NAME = 'pyodata.service' +LOGGER_NAME = "pyodata.service" HTTP_CODE_OK = 200 HTTP_CODE_CREATED = 201 @@ -33,7 +33,7 @@ def urljoin(*path): """Joins the passed string parts into a one string url""" - return '/'.join((part.strip('/') for part in path)) + return "/".join((part.strip("/") for part in path)) def encode_multipart(boundary, http_requests): @@ -41,31 +41,40 @@ def encode_multipart(boundary, http_requests): lines = [] - lines.append('') + lines.append("") for req in http_requests: - lines.append('--{0}'.format(boundary)) + lines.append("--{0}".format(boundary)) if not isinstance(req, MultipartRequest): - lines.extend(('Content-Type: application/http ', 'Content-Transfer-Encoding:binary')) + lines.extend( + ("Content-Type: application/http ", "Content-Transfer-Encoding:binary") + ) - lines.append('') + lines.append("") # request line (method + path + query params) - line = '{method} {path}'.format(method=req.get_method(), path=req.get_path()) - query_params = '&'.join(['{}={}'.format(key, val) for key, val in req.get_query_params().items()]) + line = "{method} {path}".format( + method=req.get_method(), path=req.get_path() + ) + query_params = "&".join( + [ + "{}={}".format(key, val) + for key, val in req.get_query_params().items() + ] + ) if query_params: - line += '?' + query_params - line += ' HTTP/1.1' + line += "?" + query_params + line += " HTTP/1.1" lines.append(line) # request specific headers for hdr, hdr_val in req.get_headers().items(): - lines.append('{}: {}'.format(hdr, hdr_val)) + lines.append("{}: {}".format(hdr, hdr_val)) - lines.append('') + lines.append("") body = req.get_body() if body is not None: @@ -74,11 +83,11 @@ def encode_multipart(boundary, http_requests): # this is very important since SAP gateway rejected request witout this line. It seems # blank line must be provided as a representation of emtpy body, else we are getting # 400 Bad fromat from SAP gateway - lines.append('') + lines.append("") - lines.append('--{0}--'.format(boundary)) + lines.append("--{0}--".format(boundary)) - return '\r\n'.join(lines) + return "\r\n".join(lines) def decode_multipart(data, content_type): @@ -89,7 +98,7 @@ def decode(message): messages = [] for i, part in enumerate(message.walk()): # pylint: disable=unused-variable - if part.get_content_type() == 'multipart/mixed': + if part.get_content_type() == "multipart/mixed": for submessage in part.get_payload(): messages.append(decode(submessage)) break @@ -123,7 +132,7 @@ class FakeSocket: """Fake socket to simulate received http response content""" def __init__(self, response_str): - self._file = BytesIO(response_str.encode('utf-8')) + self._file = BytesIO(response_str.encode("utf-8")) def makefile(self, *args, **kwargs): """Fake file that provides string content""" @@ -138,7 +147,9 @@ def makefile(self, *args, **kwargs): return ODataHttpResponse( response.getheaders(), response.status, - response.read(len(data)) # the len here will give a 'big enough' value to read the whole content + response.read( + len(data) + ), # the len here will give a 'big enough' value to read the whole content ) def json(self): @@ -148,7 +159,7 @@ def json(self): # approach can bring issues with encoding # https://github.com/requests/requests/blob/master/requests/models.py#L868 if self.content: - return json.loads(self.content.decode('utf-8')) + return json.loads(self.content.decode("utf-8")) return None @@ -180,9 +191,15 @@ def __init__(self, entity_type, single_key=None, **args): # check that entity type key consists of exactly one property if len(self._key) != 1: - raise PyODataException(('Key of entity type {} consists of multiple properties {} ' - 'and cannot be initialized by single value').format( - self._entity_type.name, ', '.join([prop.name for prop in self._key]))) + raise PyODataException( + ( + "Key of entity type {} consists of multiple properties {} " + "and cannot be initialized by single value" + ).format( + self._entity_type.name, + ", ".join([prop.name for prop in self._key]), + ) + ) # get single key property and format key string key_prop = self._key[0] @@ -190,12 +207,20 @@ def __init__(self, entity_type, single_key=None, **args): self._type = EntityKey.TYPE_SINGLE - self._logger.debug(('Detected single property key, adding pair %s->%s to key' - 'properties'), key_prop.name, single_key) + self._logger.debug( + ( + "Detected single property key, adding pair %s->%s to key" + "properties" + ), + key_prop.name, + single_key, + ) else: for key_prop in self._key: if key_prop.name not in args: - raise PyODataException('Missing value for key property {}'.format(key_prop.name)) + raise PyODataException( + "Missing value for key property {}".format(key_prop.name) + ) self._type = EntityKey.TYPE_COMPLEX @@ -219,14 +244,17 @@ def to_key_string_without_parentheses(self): # raise RuntimeError('Entity key is not complete, missing value of property: {0}'.format(key_prop.name)) key_pairs.append( - '{0}={1}'.format(key_prop.name, key_prop.to_literal(self._proprties[key_prop.name]))) + "{0}={1}".format( + key_prop.name, key_prop.to_literal(self._proprties[key_prop.name]) + ) + ) - return ','.join(key_pairs) + return ",".join(key_pairs) def to_key_string(self): """Gets the string representation of the key, including parentheses""" - return '({})'.format(self.to_key_string_without_parentheses()) + return "({})".format(self.to_key_string_without_parentheses()) def __repr__(self): return self.to_key_string() @@ -250,7 +278,7 @@ def handler(self): def get_path(self): """Get path of the HTTP request""" # pylint: disable=no-self-use - return '' + return "" def get_query_params(self): """Get query params""" @@ -260,7 +288,7 @@ def get_query_params(self): def get_method(self): """Get HTTP method""" # pylint: disable=no-self-use - return 'GET' + return "GET" def get_body(self): """Get HTTP body or None if not applicable""" @@ -273,6 +301,8 @@ def get_headers(self): return None def add_headers(self, value): + """Add HTTP headers to request""" + if not isinstance(value, dict): raise TypeError("Headers must be of type 'dict' not {}".format(type(value))) @@ -297,25 +327,26 @@ def execute(self): if extra_headers is not None: headers.update(extra_headers) - self._logger.debug('Send (execute) %s request to %s', self.get_method(), url) - self._logger.debug(' query params: %s', self.get_query_params()) - self._logger.debug(' headers: %s', headers) + self._logger.debug("Send (execute) %s request to %s", self.get_method(), url) + self._logger.debug(" query params: %s", self.get_query_params()) + self._logger.debug(" headers: %s", headers) if body: - self._logger.debug(' body: %s', body) + self._logger.debug(" body: %s", body) - params = "&".join("%s=%s" % (k,v) for k,v in self.get_query_params().items()) + params = "&".join("%s=%s" % (k, v) for k, v in self.get_query_params().items()) response = self._connection.request( - self.get_method(), url, headers=headers, params=params, data=body) + self.get_method(), url, headers=headers, params=params, data=body + ) - self._logger.debug('Received response') - self._logger.debug(' url: %s', response.url) - self._logger.debug(' headers: %s', response.headers) - self._logger.debug(' status code: %d', response.status_code) + self._logger.debug("Received response") + self._logger.debug(" url: %s", response.url) + self._logger.debug(" headers: %s", response.headers) + self._logger.debug(" status code: %d", response.status_code) try: - self._logger.debug(' body: %s', response.content.decode('utf-8')) + self._logger.debug(" body: %s", response.content.decode("utf-8")) except UnicodeDecodeError: - self._logger.debug(' body: ') + self._logger.debug(" body: ") return self._handler(response) @@ -324,15 +355,19 @@ class EntityGetRequest(ODataHttpRequest): """Used for GET operations of a single entity""" def __init__(self, handler, entity_key, entity_set_proxy): - super(EntityGetRequest, self).__init__(entity_set_proxy.service.url, entity_set_proxy.service.connection, - handler) + super(EntityGetRequest, self).__init__( + entity_set_proxy.service.url, entity_set_proxy.service.connection, handler + ) self._logger = logging.getLogger(LOGGER_NAME) self._entity_key = entity_key self._entity_set_proxy = entity_set_proxy self._select = None self._expand = None - self._logger.debug('New instance of EntityGetRequest for last segment: %s', self._entity_set_proxy.last_segment) + self._logger.debug( + "New instance of EntityGetRequest for last segment: %s", + self._entity_set_proxy.last_segment, + ) def nav(self, nav_property): """Navigates to given navigation property and returns the EntitySetProxy""" @@ -358,16 +393,16 @@ 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', **self._headers} + return {"Accept": "application/json", **self._headers} def get_query_params(self): qparams = super(EntityGetRequest, self).get_query_params() if self._select is not None: - qparams['$select'] = self._select + qparams["$select"] = self._select if self._expand is not None: - qparams['$expand'] = self._expand + qparams["$expand"] = self._expand return qparams @@ -381,15 +416,18 @@ def stream_handler(response): """Returns $value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for $value failed with status code {}' - .format(response.status_code), response) + raise HttpError( + "HTTP GET for $value failed with status code {}".format( + response.status_code + ), + response, + ) return response return ODataHttpRequest( - urljoin(self._url, self.get_path(), '/$value'), - connection, - stream_handler) + urljoin(self._url, self.get_path(), "/$value"), connection, stream_handler + ) class NavEntityGetRequest(EntityGetRequest): @@ -401,7 +439,9 @@ def __init__(self, handler, master_key, entity_set_proxy, nav_property): self._nav_property = nav_property def get_path(self): - return "{}/{}".format(super(NavEntityGetRequest, self).get_path(), self._nav_property) + return "{}/{}".format( + super(NavEntityGetRequest, self).get_path(), self._nav_property + ) class EntityCreateRequest(ODataHttpRequest): @@ -426,15 +466,18 @@ def __init__(self, url, connection, handler, entity_set, last_segment=None): # get all properties declared by entity type self._type_props = self._entity_type.proprties() - self._logger.debug('New instance of EntityCreateRequest for entity type: %s on path %s', self._entity_type.name, - self._last_segment) + self._logger.debug( + "New instance of EntityCreateRequest for entity type: %s on path %s", + self._entity_type.name, + self._last_segment, + ) def get_path(self): return self._last_segment def get_method(self): # pylint: disable=no-self-use - return 'POST' + return "POST" def _get_body(self): """Recursively builds a dictionary of values where some of the values @@ -455,7 +498,12 @@ 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', **self._headers} + return { + "Accept": "application/json", + "Content-Type": "application/json", + "X-Requested-With": "X", + **self._headers, + } @staticmethod def _build_values(entity_type, entity): @@ -465,7 +513,9 @@ def _build_values(entity_type, entity): """ if isinstance(entity, list): - return [EntityCreateRequest._build_values(entity_type, item) for item in entity] + return [ + EntityCreateRequest._build_values(entity_type, item) for item in entity + ] values = {} for key, val in entity.items(): @@ -476,8 +526,11 @@ def _build_values(entity_type, entity): nav_prop = entity_type.nav_proprty(key) val = EntityCreateRequest._build_values(nav_prop.typ, val) except KeyError: - raise PyODataException('Property {} is not declared in {} entity type'.format( - key, entity_type.name)) + raise PyODataException( + "Property {} is not declared in {} entity type".format( + key, entity_type.name + ) + ) values[key] = val @@ -503,14 +556,17 @@ def __init__(self, url, connection, handler, entity_set, entity_key): self._entity_set = entity_set self._entity_key = entity_key - self._logger.debug('New instance of EntityDeleteRequest for entity type: %s', entity_set.entity_type.name) + self._logger.debug( + "New instance of EntityDeleteRequest for entity type: %s", + entity_set.entity_type.name, + ) def get_path(self): return self._entity_set.name + self._entity_key.to_key_string() def get_method(self): # pylint: disable=no-self-use - return 'DELETE' + return "DELETE" class EntityModifyRequest(ODataHttpRequest): @@ -519,9 +575,11 @@ class EntityModifyRequest(ODataHttpRequest): Call execute() to send the update-request to the OData service and get the modified entity.""" - ALLOWED_HTTP_METHODS = ['PATCH', 'PUT', 'MERGE'] + ALLOWED_HTTP_METHODS = ["PATCH", "PUT", "MERGE"] - def __init__(self, url, connection, handler, entity_set, entity_key, method="PATCH"): + 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 @@ -530,15 +588,21 @@ def __init__(self, url, connection, handler, entity_set, entity_key, method="PAT self._method = method.upper() if self._method not in EntityModifyRequest.ALLOWED_HTTP_METHODS: - raise ValueError('The value "{}" is not on the list of allowed Entity Update HTTP Methods: {}' - .format(method, ', '.join(EntityModifyRequest.ALLOWED_HTTP_METHODS))) + raise ValueError( + 'The value "{}" is not on the list of allowed Entity Update HTTP Methods: {}'.format( + method, ", ".join(EntityModifyRequest.ALLOWED_HTTP_METHODS) + ) + ) self._values = {} # get all properties declared by entity type self._type_props = self._entity_type.proprties() - self._logger.debug('New instance of EntityModifyRequest for entity type: %s', self._entity_type.name) + self._logger.debug( + "New instance of EntityModifyRequest for entity type: %s", + self._entity_type.name, + ) def get_path(self): return self._entity_set.name + self._entity_key.to_key_string() @@ -555,7 +619,11 @@ def get_body(self): return json.dumps(body) def get_headers(self): - return {'Accept': 'application/json', 'Content-Type': 'application/json', **self._headers} + return { + "Accept": "application/json", + "Content-Type": "application/json", + **self._headers, + } def set(self, **kwargs): """Set properties to be changed.""" @@ -567,7 +635,10 @@ def set(self, **kwargs): val = self._entity_type.proprty(key).to_json(val) except KeyError: raise PyODataException( - 'Property {} is not declared in {} entity type'.format(key, self._entity_type.name)) + "Property {} is not declared in {} entity type".format( + key, self._entity_type.name + ) + ) self._values[key] = val @@ -592,7 +663,9 @@ def __init__(self, url, connection, handler, last_segment): self._expand = None self._last_segment = last_segment self._customs = {} # string -> string hash - self._logger.debug('New instance of QueryRequest for last segment: %s', self._last_segment) + self._logger.debug( + "New instance of QueryRequest for last segment: %s", self._last_segment + ) def custom(self, name, value): """Adds a custom name-value pair.""" @@ -643,7 +716,7 @@ def top(self, top): def get_path(self): if self._count: - return urljoin(self._last_segment, '/$count') + return urljoin(self._last_segment, "/$count") return self._last_segment @@ -651,34 +724,31 @@ def get_headers(self): if self._count: return {} - return { - 'Accept': 'application/json', - **self._headers - } + return {"Accept": "application/json", **self._headers} def get_query_params(self): qparams = super(QueryRequest, self).get_query_params() if self._top is not None: - qparams['$top'] = self._top + qparams["$top"] = self._top if self._skip is not None: - qparams['$skip'] = self._skip + qparams["$skip"] = self._skip if self._order_by is not None: - qparams['$orderby'] = self._order_by + qparams["$orderby"] = self._order_by if self._filter is not None: - qparams['$filter'] = self._filter + qparams["$filter"] = self._filter if self._select is not None: - qparams['$select'] = self._select + qparams["$select"] = self._select for key, val in self._customs.items(): qparams[key] = val if self._expand is not None: - qparams['$expand'] = self._expand + qparams["$expand"] = self._expand return qparams @@ -687,14 +757,18 @@ class FunctionRequest(QueryRequest): """Function import request (Service call)""" def __init__(self, url, connection, handler, function_import): - super(FunctionRequest, self).__init__(url, connection, handler, function_import.name) + super(FunctionRequest, self).__init__( + url, connection, handler, function_import.name + ) self._function_import = function_import - self._logger.debug('New instance of FunctionRequest for %s', self._function_import.name) + self._logger.debug( + "New instance of FunctionRequest for %s", self._function_import.name + ) def parameter(self, name, value): - '''Sets value of parameter.''' + """Sets value of parameter.""" # check if param is valid (is declared in metadata) try: @@ -703,8 +777,11 @@ def parameter(self, name, value): # add parameter as custom query argument self.custom(param.name, param.to_literal(value)) except KeyError: - raise PyODataException('Function import {0} does not have pararmeter {1}' - .format(self._function_import.name, name)) + raise PyODataException( + "Function import {0} does not have pararmeter {1}".format( + self._function_import.name, name + ) + ) return self @@ -712,10 +789,7 @@ def get_method(self): return self._function_import.http_method def get_headers(self): - return { - 'Accept': 'application/json', - **self._headers - } + return {"Accept": "application/json", **self._headers} class EntityProxy: @@ -726,7 +800,9 @@ class EntityProxy: # pylint: disable=too-many-branches,too-many-nested-blocks - def __init__(self, service, entity_set, entity_type, proprties=None, entity_key=None): + def __init__( + self, service, entity_set, entity_type, proprties=None, entity_key=None + ): self._logger = logging.getLogger(LOGGER_NAME) self._service = service self._entity_set = entity_set @@ -735,7 +811,11 @@ def __init__(self, service, entity_set, entity_type, proprties=None, entity_key= self._cache = dict() self._entity_key = entity_key - self._logger.debug('New entity proxy instance of type %s from properties: %s', entity_type.name, proprties) + self._logger.debug( + "New entity proxy instance of type %s from properties: %s", + entity_type.name, + proprties, + ) # cache values of individual properties if provided if proprties is not None: @@ -744,10 +824,14 @@ def __init__(self, service, entity_set, entity_type, proprties=None, entity_key= for type_proprty in self._entity_type.proprties(): if type_proprty.name in proprties: if proprties[type_proprty.name] is not None: - self._cache[type_proprty.name] = type_proprty.from_json(proprties[type_proprty.name]) + self._cache[type_proprty.name] = type_proprty.from_json( + proprties[type_proprty.name] + ) else: # null value is in literal form for now, convert it to python representation - self._cache[type_proprty.name] = type_proprty.from_literal(type_proprty.typ.null_value) + self._cache[type_proprty.name] = type_proprty.from_literal( + type_proprty.typ.null_value + ) # then, assign all navigation properties for prop in self._entity_type.nav_proprties: @@ -758,30 +842,41 @@ def __init__(self, service, entity_set, entity_type, proprties=None, entity_key= prop_etype = prop.to_role.entity_type # cache value according to multiplicity - if prop.to_role.multiplicity in \ - [model.EndRole.MULTIPLICITY_ONE, - model.EndRole.MULTIPLICITY_ZERO_OR_ONE]: + if prop.to_role.multiplicity in [ + model.EndRole.MULTIPLICITY_ONE, + model.EndRole.MULTIPLICITY_ZERO_OR_ONE, + ]: # cache None in case we receive nothing (null) instead of entity data if proprties[prop.name] is None: self._cache[prop.name] = None else: - self._cache[prop.name] = EntityProxy(service, None, prop_etype, proprties[prop.name]) - - elif prop.to_role.multiplicity == model.EndRole.MULTIPLICITY_ZERO_OR_MORE: + self._cache[prop.name] = EntityProxy( + service, None, prop_etype, proprties[prop.name] + ) + + elif ( + prop.to_role.multiplicity + == model.EndRole.MULTIPLICITY_ZERO_OR_MORE + ): # default value is empty array self._cache[prop.name] = [] # if there are no entities available, received data consists of # metadata properties only. - if 'results' in proprties[prop.name]: + if "results" in proprties[prop.name]: # available entities are serialized in results array - for entity in proprties[prop.name]['results']: - self._cache[prop.name].append(EntityProxy(service, None, prop_etype, entity)) + for entity in proprties[prop.name]["results"]: + self._cache[prop.name].append( + EntityProxy(service, None, prop_etype, entity) + ) else: - raise PyODataException('Unknown multiplicity {0} of association role {1}' - .format(prop.to_role.multiplicity, prop.to_role.name)) + raise PyODataException( + "Unknown multiplicity {0} of association role {1}".format( + prop.to_role.multiplicity, prop.to_role.name + ) + ) # build entity key if not provided if self._entity_key is None: @@ -789,7 +884,9 @@ def __init__(self, service, entity_set, entity_type, proprties=None, entity_key= try: # if key seems to be simple (consists of single property) if len(self._key_props) == 1: - self._entity_key = EntityKey(entity_type, self._cache[self._key_props[0].name]) + self._entity_key = EntityKey( + entity_type, self._cache[self._key_props[0].name] + ) else: # build complex key self._entity_key = EntityKey(entity_type, **self._cache) @@ -810,8 +907,11 @@ def __getattr__(self, attr): self._cache[attr] = value return value except KeyError as ex: - raise AttributeError('EntityType {0} does not have Property {1}: {2}' - .format(self._entity_type.name, attr, str(ex))) + raise AttributeError( + "EntityType {0} does not have Property {1}: {2}".format( + self._entity_type.name, attr, str(ex) + ) + ) def nav(self, nav_property): """Navigates to given navigation property and returns the EntitySetProxy""" @@ -820,58 +920,83 @@ def nav(self, nav_property): try: navigation_property = self._entity_type.nav_proprty(nav_property) except KeyError: - raise PyODataException('Navigation property {} is not declared in {} entity type'.format( - nav_property, self._entity_type)) + raise PyODataException( + "Navigation property {} is not declared in {} entity type".format( + nav_property, self._entity_type + ) + ) # Get entity set of navigation property association_info = navigation_property.association_info association_set = self._service.schema.association_set_by_association( - association_info.name, - association_info.namespace) + association_info.name, association_info.namespace + ) navigation_entity_set = None for end in association_set.end_roles: - if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role: - navigation_entity_set = self._service.schema.entity_set(end.entity_set_name, association_info.namespace) + if ( + association_set.end_by_entity_set(end.entity_set_name).role + == navigation_property.to_role.role + ): + navigation_entity_set = self._service.schema.entity_set( + end.entity_set_name, association_info.namespace + ) if not navigation_entity_set: - raise PyODataException('No association set for role {}'.format(navigation_property.to_role)) + raise PyODataException( + "No association set for role {}".format(navigation_property.to_role) + ) roles = navigation_property.association.end_roles - if all((role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE for role in roles)): - return NavEntityProxy(self, nav_property, navigation_entity_set.entity_type, {}) + if all( + ( + role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE + for role in roles + ) + ): + return NavEntityProxy( + self, nav_property, navigation_entity_set.entity_type, {} + ) return EntitySetProxy( self._service, self._service.schema.entity_set(navigation_entity_set.name), nav_property, - self._entity_set.name + self._entity_key.to_key_string()) + self._entity_set.name + self._entity_key.to_key_string(), + ) def get_path(self): """Returns this entity's relative path - e.g. EntitySet(KEY)""" - return self._entity_set._name + self._entity_key.to_key_string() # pylint: disable=protected-access + return ( + self._entity_set._name + self._entity_key.to_key_string() + ) # pylint: disable=protected-access def get_proprty(self, name, connection=None): """Returns value of the property""" - self._logger.info('Initiating property request for %s', name) + self._logger.info("Initiating property request for %s", name) def proprty_get_handler(key, proprty, response): """Gets property value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for Attribute {0} of Entity {1} failed with status code {2}' - .format(proprty.name, key, response.status_code), response) + raise HttpError( + "HTTP GET for Attribute {0} of Entity {1} failed with status code {2}".format( + proprty.name, key, response.status_code + ), + response, + ) - data = response.json()['d'] + data = response.json()["d"] return proprty.from_json(data[proprty.name]) path = urljoin(self.get_path(), name) return self._service.http_get_odata( path, partial(proprty_get_handler, path, self._entity_type.proprty(name)), - connection=connection) + connection=connection, + ) def get_value(self, connection=None): "Returns $value of Stream entities" @@ -880,15 +1005,19 @@ def value_get_handler(key, response): """Gets property value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for $value of Entity {0} failed with status code {1}' - .format(key, response.status_code), response) + raise HttpError( + "HTTP GET for $value of Entity {0} failed with status code {1}".format( + key, response.status_code + ), + response, + ) return response - path = urljoin(self.get_path(), '/$value') - return self._service.http_get_odata(path, - partial(value_get_handler, self.entity_key), - connection=connection) + path = urljoin(self.get_path(), "/$value") + return self._service.http_get_odata( + path, partial(value_get_handler, self.entity_key), connection=connection + ) @property def entity_set(self): @@ -906,7 +1035,7 @@ def entity_key(self): def url(self): """URL of the real entity""" - service_url = self._service.url.rstrip('/') + service_url = self._service.url.rstrip("/") entity_path = self.get_path() return urljoin(service_url, entity_path) @@ -922,7 +1051,9 @@ class NavEntityProxy(EntityProxy): def __init__(self, parent_entity, prop_name, entity_type, entity): # pylint: disable=protected-access - super(NavEntityProxy, self).__init__(parent_entity._service, parent_entity._entity_set, entity_type, entity) + super(NavEntityProxy, self).__init__( + parent_entity._service, parent_entity._entity_set, entity_type, entity + ) self._parent_entity = parent_entity self._prop_name = prop_name @@ -944,47 +1075,52 @@ def build_expression(operator, operands): """Creates a expression by joining the operands with the operator""" if len(operands) < 2: - raise ExpressionError('The $filter operator \'{}\' needs at least two operands'.format(operator)) + raise ExpressionError( + "The $filter operator '{}' needs at least two operands".format(operator) + ) - return '({})'.format(' {} '.format(operator).join(operands)) + return "({})".format(" {} ".format(operator).join(operands)) @staticmethod def and_(*operands): """Creates logical AND expression from the operands""" - return GetEntitySetFilter.build_expression('and', operands) + return GetEntitySetFilter.build_expression("and", operands) @staticmethod def or_(*operands): """Creates logical OR expression from the operands""" - return GetEntitySetFilter.build_expression('or', operands) + return GetEntitySetFilter.build_expression("or", operands) @staticmethod def format_filter(proprty, operator, value): """Creates a filter expression """ - return '{} {} {}'.format(proprty.name, operator, proprty.to_literal(value)) + return "{} {} {}".format(proprty.name, operator, proprty.to_literal(value)) def __eq__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'eq', value) + return GetEntitySetFilter.format_filter(self._proprty, "eq", value) def __ne__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'ne', value) + return GetEntitySetFilter.format_filter(self._proprty, "ne", value) def __lt__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'lt', value) + return GetEntitySetFilter.format_filter(self._proprty, "lt", value) def __le__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'le', value) + return GetEntitySetFilter.format_filter(self._proprty, "le", value) def __ge__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'ge', value) + return GetEntitySetFilter.format_filter(self._proprty, "ge", value) + def __gt__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, 'gt', value) + return GetEntitySetFilter.format_filter(self._proprty, "gt", value) -class FilterExpression(object): - def __init__(self, *args, **kwargs): + +class FilterExpression: + """A filter expression object comparable to Django's Q class""" + def __init__(self, **kwargs): self.expressions = kwargs self.other = None self.operator = None @@ -999,7 +1135,8 @@ def __and__(self, other): self.operator = "and" return self -class GetEntitySetFilterChainable(object): + +class GetEntitySetFilterChainable: """ Example expressions FirstName="Tim" @@ -1015,36 +1152,54 @@ class GetEntitySetFilterChainable(object): Addresses__Suburb="Chatswood" Addresses__Suburb__contains="wood" """ - operators = ["startswith", "endswith", "lt", "lte", "gt", "gte", "contains", "range", "in", "length"] + + operators = [ + "startswith", + "endswith", + "lt", + "lte", + "gt", + "gte", + "contains", + "range", + "in", + "length", + ] + def __init__(self, request, filter_expressions, exprs): self.request = request self.expressions = exprs self.filter_expressions = filter_expressions def proprty_obj(self, name): - return self.request._entity_type.proprty(name) + """Returns value for a particular proprty""" + return self.request._entity_type.proprty(name) # pylint: disable=protected-access def process_query_objects(self): + """Processes FilterExpression objects to OData lookups""" filter_expressions = [] - for q in self.filter_expressions: + for filter_expression in self.filter_expressions: lhs_expressions = [] rhs_expressions = [] - for expr, val in q.expressions.items(): + for expr, val in filter_expression.expressions.items(): lhs_expressions.append(self.decode_expression(expr, val)) - lhs_expressions = self.combine_expressions(lhs_expressions) + lhs_expression = self.combine_expressions(lhs_expressions) - if q.other: - for expr, val in q.other.expressions.items(): + if filter_expression.other: + for expr, val in filter_expression.other.expressions.items(): rhs_expressions.append(self.decode_expression(expr, val)) - rhs_expressions = self.combine_expressions(rhs_expressions) - - filter_expressions.append(f"({lhs_expressions}) {q.operator} ({rhs_expressions})") + rhs_expression = self.combine_expressions(rhs_expressions) + + filter_expressions.append( + f"({lhs_expression}) {filter_expression.operator} ({rhs_expression})" + ) else: filter_expressions.append(lhs_expression) return filter_expressions def process_expressions(self): + """Processes filter kwargs into OData expressions""" filter_expressions = [] for expr, val in self.expressions.items(): filter_expressions.append(self.decode_expression(expr, val)) @@ -1053,7 +1208,8 @@ def process_expressions(self): return filter_expressions def decode_expression(self, expr, val): - properties = self.request._entity_type._properties.keys() + """Decodes Django-like syntax into OData expressions""" + properties = self.request._entity_type._properties.keys() # pylint: disable=protected-access field = None # field_heirarchy = [] operator = "eq" @@ -1070,13 +1226,17 @@ def decode_expression(self, expr, val): # target_field = self.proprty_obj(field_heirarchy[-1]) expression = self.build_expression(field, operator, val) - + return expression - + def combine_expressions(self, expressions): + """Combines expressions""" + # pylint: disable=no-self-use return " and ".join(expressions) def build_expression(self, field_name, operator, value): + """Builds expression from Django-like operator""" + # pylint: disable=too-many-branches, too-many-return-statements, no-else-return target_field = self.proprty_obj(field_name) if operator not in ["length", "in", "range"]: value = target_field.to_literal(value) @@ -1098,18 +1258,20 @@ def build_expression(self, field_name, operator, value): elif operator in ["contains"]: return f"substringof({value}, {field_name}) eq true" elif operator == "range": - if not (isinstance(value, tuple) or isinstance(value, list)): - raise TypeError("Range must be tuple or list not {}".format(type(value))) + if not isinstance(value, (tuple, list)): + raise TypeError( + "Range must be tuple or list not {}".format(type(value)) + ) if len(value) != 2: raise ValueError("Only two items can be passed in a range.") - - x = target_field.to_literal(value[0]) - y = target_field.to_literal(value[1]) - return f"{field_name} gte {x} and {field_name} lte {y}" + + value_0 = target_field.to_literal(value[0]) + value_1 = target_field.to_literal(value[1]) + return f"{field_name} gte {value_0} and {field_name} lte {value_1}" elif operator == "in": literal_values = [] - for v in value: - val = target_field.to_literal(v) + for val in value: + val = target_field.to_literal(val) literal_values.append(f"{field_name} eq {val}") return " or ".join(literal_values) elif operator == "eq": @@ -1118,15 +1280,19 @@ def build_expression(self, field_name, operator, value): raise ValueError(f"Invalid expression {operator}") def as_filter_string(self): + """Returns final filter string for this filter""" expressions = self.process_expressions() result = self.combine_expressions(expressions) return quote(result) + class GetEntitySetRequest(QueryRequest): """GET on EntitySet""" def __init__(self, url, connection, handler, last_segment, entity_type): - super(GetEntitySetRequest, self).__init__(url, connection, handler, last_segment) + super(GetEntitySetRequest, self).__init__( + url, connection, handler, last_segment + ) self._entity_type = entity_type @@ -1135,16 +1301,20 @@ def __getattr__(self, name): return GetEntitySetFilter(proprty) def set_filter(self, filter_val): + """Chain filter""" filter_text = self._filter + " and " if self._filter else "" filter_text += filter_val self._filter = filter_text def filter(self, *args, **kwargs): - if len(args) and isinstance(args[0], str): + # pylint: disable=no-else-return + if args and isinstance(args[0], str): self._filter = args[0] return self else: - self.set_filter(GetEntitySetFilterChainable(self, args, kwargs).as_filter_string()) + self.set_filter( + GetEntitySetFilterChainable(self, args, kwargs).as_filter_string() + ) return self @@ -1161,17 +1331,17 @@ def __init__(self, service, entity_set, alias=None, parent_last_segment=None): self._entity_set = entity_set self._alias = alias if parent_last_segment is None: - self._parent_last_segment = '' + self._parent_last_segment = "" else: - if parent_last_segment.endswith('/'): + if parent_last_segment.endswith("/"): self._parent_last_segment = parent_last_segment else: - self._parent_last_segment = parent_last_segment + '/' + self._parent_last_segment = parent_last_segment + "/" self._name = entity_set.name self._key = entity_set.entity_type.key_proprties self._logger = logging.getLogger(LOGGER_NAME) - self._logger.debug('New entity set proxy instance for %s', self._name) + self._logger.debug("New entity set proxy instance for %s", self._name) @property def service(self): @@ -1182,7 +1352,9 @@ def service(self): def last_segment(self): """Return last segment of url""" - entity_set_name = self._alias if self._alias is not None else self._entity_set.name + entity_set_name = ( + self._alias if self._alias is not None else self._entity_set.name + ) return self._parent_last_segment + entity_set_name def nav(self, nav_property, key): @@ -1191,32 +1363,50 @@ def nav(self, nav_property, key): try: navigation_property = self._entity_set.entity_type.nav_proprty(nav_property) except KeyError: - raise PyODataException('Navigation property {} is not declared in {} entity type'.format( - nav_property, self._entity_set.entity_type)) + raise PyODataException( + "Navigation property {} is not declared in {} entity type".format( + nav_property, self._entity_set.entity_type + ) + ) # Get entity set of navigation property association_info = navigation_property.association_info association_set = self._service.schema.association_set_by_association( - association_info.name) + association_info.name + ) navigation_entity_set = None for end in association_set.end_roles: - if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role: - navigation_entity_set = self._service.schema.entity_set(end.entity_set_name) + if ( + association_set.end_by_entity_set(end.entity_set_name).role + == navigation_property.to_role.role + ): + navigation_entity_set = self._service.schema.entity_set( + end.entity_set_name + ) if not navigation_entity_set: raise PyODataException( - 'No association set for role {} {}'.format(navigation_property.to_role, association_set.end_roles)) + "No association set for role {} {}".format( + navigation_property.to_role, association_set.end_roles + ) + ) roles = navigation_property.association.end_roles - if all((role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE for role in roles)): + if all( + ( + role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE + for role in roles + ) + ): return self._get_nav_entity(key, nav_property, navigation_entity_set) return EntitySetProxy( self._service, navigation_entity_set, nav_property, - self._entity_set.name + key.to_key_string()) + self._entity_set.name + key.to_key_string(), + ) def _get_nav_entity(self, master_key, nav_property, navigation_entity_set): """Get entity based on provided key of the master and Navigation property name""" @@ -1225,26 +1415,36 @@ def get_entity_handler(parent, nav_property, navigation_entity_set, response): """Gets entity from HTTP response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for Entity {0} failed with status code {1}' - .format(self._name, response.status_code), response) + raise HttpError( + "HTTP GET for Entity {0} failed with status code {1}".format( + self._name, response.status_code + ), + response, + ) - entity = response.json()['d'] + entity = response.json()["d"] - return NavEntityProxy(parent, nav_property, navigation_entity_set.entity_type, entity) + return NavEntityProxy( + parent, nav_property, navigation_entity_set.entity_type, entity + ) self._logger.info( - 'Getting the nav property %s of the entity %s for the key %s', + "Getting the nav property %s of the entity %s for the key %s", nav_property, self._entity_set.entity_type.name, - master_key) + master_key, + ) - parent = EntityProxy(self._service, self, self._entity_set.entity_type, entity_key=master_key) + parent = EntityProxy( + self._service, self, self._entity_set.entity_type, entity_key=master_key + ) return NavEntityGetRequest( partial(get_entity_handler, parent, nav_property, navigation_entity_set), master_key, self, - nav_property) + nav_property, + ) def get_entity(self, key=None, **args): """Get entity based on provided key properties""" @@ -1253,19 +1453,30 @@ def get_entity_handler(response): """Gets entity from HTTP response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for Entity {0} failed with status code {1}' - .format(self._name, response.status_code), response) + raise HttpError( + "HTTP GET for Entity {0} failed with status code {1}".format( + self._name, response.status_code + ), + response, + ) - entity = response.json()['d'] + entity = response.json()["d"] - return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity) + return EntityProxy( + self._service, self._entity_set, self._entity_set.entity_type, entity + ) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **args) - self._logger.info('Getting entity %s for key %s and args %s', self._entity_set.entity_type.name, key, args) + self._logger.info( + "Getting entity %s for key %s and args %s", + self._entity_set.entity_type.name, + key, + args, + ) return EntityGetRequest(get_entity_handler, entity_key, self) @@ -1276,30 +1487,43 @@ def get_entities_handler(response): """Gets entity set from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError('HTTP GET for Entity Set {0} failed with status code {1}' - .format(self._name, response.status_code), response) + raise HttpError( + "HTTP GET for Entity Set {0} failed with status code {1}".format( + self._name, response.status_code + ), + response, + ) content = response.json() if isinstance(content, int): return content - entities = content['d'] + entities = content["d"] if isinstance(entities, dict): - entities = entities['results'] + entities = entities["results"] - self._logger.info('Fetched %d entities', len(entities)) + self._logger.info("Fetched %d entities", len(entities)) result = [] for props in entities: - entity = EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, props) + entity = EntityProxy( + self._service, self._entity_set, self._entity_set.entity_type, props + ) result.append(entity) return result - entity_set_name = self._alias if self._alias is not None else self._entity_set.name - return GetEntitySetRequest(self._service.url, self._service.connection, get_entities_handler, - self._parent_last_segment + entity_set_name, self._entity_set.entity_type) + entity_set_name = ( + self._alias if self._alias is not None else self._entity_set.name + ) + return GetEntitySetRequest( + self._service.url, + self._service.connection, + get_entities_handler, + self._parent_last_segment + entity_set_name, + self._entity_set.entity_type, + ) def create_entity(self, return_code=HTTP_CODE_CREATED): """Creates a new entity in the given entity-set.""" @@ -1308,15 +1532,29 @@ def create_entity_handler(response): """Gets newly created entity encoded in HTTP Response""" if response.status_code != return_code: - raise HttpError('HTTP POST for Entity Set {0} failed with status code {1}' - .format(self._name, response.status_code), response) - - entity_props = response.json()['d'] - - return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity_props) - - return EntityCreateRequest(self._service.url, self._service.connection, create_entity_handler, self._entity_set, - self.last_segment) + raise HttpError( + "HTTP POST for Entity Set {0} failed with status code {1}".format( + self._name, response.status_code + ), + response, + ) + + entity_props = response.json()["d"] + + return EntityProxy( + self._service, + self._entity_set, + self._entity_set.entity_type, + entity_props, + ) + + return EntityCreateRequest( + self._service.url, + self._service.connection, + create_entity_handler, + self._entity_set, + self.last_segment, + ) def update_entity(self, key=None, method=None, **kwargs): """Updates an existing entity in the given entity-set.""" @@ -1325,21 +1563,36 @@ def update_entity_handler(response): """Gets modified entity encoded in HTTP Response""" if response.status_code != 204: - raise HttpError('HTTP modify request for Entity Set {} failed with status code {}' - .format(self._name, response.status_code), response) + raise HttpError( + "HTTP modify request for Entity Set {} failed with status code {}".format( + self._name, response.status_code + ), + response, + ) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **kwargs) - self._logger.info('Updating entity %s for key %s and args %s', self._entity_set.entity_type.name, key, kwargs) + self._logger.info( + "Updating entity %s for key %s and args %s", + self._entity_set.entity_type.name, + key, + kwargs, + ) if method is None: - method = self._service.config['http']['update_method'] - - return EntityModifyRequest(self._service.url, self._service.connection, update_entity_handler, self._entity_set, - entity_key, method=method) + method = self._service.config["http"]["update_method"] + + return EntityModifyRequest( + self._service.url, + self._service.connection, + update_entity_handler, + self._entity_set, + entity_key, + method=method, + ) def delete_entity(self, key: EntityKey = None, **kwargs): """Delete the entity""" @@ -1348,17 +1601,24 @@ def delete_entity_handler(response): """Check if entity deletion was successful""" if response.status_code != 204: - raise HttpError(f'HTTP POST for Entity delete {self._name} ' - f'failed with status code {response.status_code}', - response) + raise HttpError( + f"HTTP POST for Entity delete {self._name} " + f"failed with status code {response.status_code}", + response, + ) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **kwargs) - return EntityDeleteRequest(self._service.url, self._service.connection, delete_entity_handler, self._entity_set, - entity_key) + return EntityDeleteRequest( + self._service.url, + self._service.connection, + delete_entity_handler, + self._entity_set, + entity_key, + ) # pylint: disable=too-few-public-methods @@ -1371,14 +1631,19 @@ def __init__(self, service): self._entity_sets = dict() for entity_set in self._service.schema.entity_sets: - self._entity_sets[entity_set.name] = EntitySetProxy(self._service, entity_set) + self._entity_sets[entity_set.name] = EntitySetProxy( + self._service, entity_set + ) def __getattr__(self, name): try: return self._entity_sets[name] except KeyError: raise AttributeError( - 'EntitySet {0} not defined in {1}.'.format(name, ','.join(list(self._entity_sets.keys())))) + "EntitySet {0} not defined in {1}.".format( + name, ",".join(list(self._entity_sets.keys())) + ) + ) class FunctionContainer: @@ -1399,7 +1664,10 @@ def __getattr__(self, name): if name not in self._functions: raise AttributeError( - 'Function {0} not defined in {1}.'.format(name, ','.join(list(self._functions.keys())))) + "Function {0} not defined in {1}.".format( + name, ",".join(list(self._functions.keys())) + ) + ) fimport = self._service.schema.function_import(name) @@ -1407,60 +1675,82 @@ def function_import_handler(fimport, response): """Get function call response from HTTP Response""" if 300 <= response.status_code < 400: - raise HttpError(f'Function Import {fimport.name} requires Redirection which is not supported', - response) + raise HttpError( + f"Function Import {fimport.name} requires Redirection which is not supported", + response, + ) if response.status_code == 401: - raise HttpError(f'Not authorized to call Function Import {fimport.name}', - response) + raise HttpError( + f"Not authorized to call Function Import {fimport.name}", response + ) if response.status_code == 403: - raise HttpError(f'Missing privileges to call Function Import {fimport.name}', - response) + raise HttpError( + f"Missing privileges to call Function Import {fimport.name}", + response, + ) if response.status_code == 405: raise HttpError( - f'Despite definition Function Import {fimport.name} does not support HTTP {fimport.http_method}', - response) + f"Despite definition Function Import {fimport.name} does not support HTTP {fimport.http_method}", + response, + ) if 400 <= response.status_code < 500: raise HttpError( - f'Function Import {fimport.name} call has failed with status code {response.status_code}', - response) + f"Function Import {fimport.name} call has failed with status code {response.status_code}", + response, + ) if response.status_code >= 500: - raise HttpError(f'Server has encountered an error while processing Function Import {fimport.name}', - response) + raise HttpError( + f"Server has encountered an error while processing Function Import {fimport.name}", + response, + ) if fimport.return_type is None: if response.status_code != 204: logging.getLogger(LOGGER_NAME).warning( - 'The No Return Function Import %s has replied with HTTP Status Code %d instead of 204', - fimport.name, response.status_code) + "The No Return Function Import %s has replied with HTTP Status Code %d instead of 204", + fimport.name, + response.status_code, + ) if response.text: logging.getLogger(LOGGER_NAME).warning( - 'The No Return Function Import %s has returned content:\n%s', fimport.name, response.text) + "The No Return Function Import %s has returned content:\n%s", + fimport.name, + response.text, + ) return None if response.status_code != 200: logging.getLogger(LOGGER_NAME).warning( - 'The Function Import %s has replied with HTTP Status Code %d instead of 200', - fimport.name, response.status_code) + "The Function Import %s has replied with HTTP Status Code %d instead of 200", + fimport.name, + response.status_code, + ) - response_data = response.json()['d'] + response_data = response.json()["d"] # 1. if return types is "entity type", return instance of appropriate entity proxy if isinstance(fimport.return_type, model.EntityType): entity_set = self._service.schema.entity_set(fimport.entity_set_name) - return EntityProxy(self._service, entity_set, fimport.return_type, response_data) + return EntityProxy( + self._service, entity_set, fimport.return_type, response_data + ) # 2. return raw data for all other return types (primitives, complex types encoded in dicts, etc.) return response_data - return FunctionRequest(self._service.url, self._service.connection, - partial(function_import_handler, fimport), fimport) + return FunctionRequest( + self._service.url, + self._service.connection, + partial(function_import_handler, fimport), + fimport, + ) class Service: @@ -1473,7 +1763,7 @@ def __init__(self, url, schema, connection): self._entity_container = EntityContainer(self) self._function_container = FunctionContainer(self) - self._config = {'http': {'update_method': 'PATCH'}} + self._config = {"http": {"update_method": "PATCH"}} @property def schema(self): @@ -1531,7 +1821,8 @@ def http_get_odata(self, path, handler, connection=None): urljoin(self._url, path), conn, handler, - headers={'Accept': 'application/json'}) + headers={"Accept": "application/json"}, + ) def create_batch(self, batch_id=None): """Create instance of OData batch request""" @@ -1539,11 +1830,15 @@ def create_batch(self, batch_id=None): def batch_handler(batch, parts): """Process parsed multipart request (parts)""" - logging.getLogger(LOGGER_NAME).debug('Batch handler called for batch %s', batch.id) + logging.getLogger(LOGGER_NAME).debug( + "Batch handler called for batch %s", batch.id + ) result = [] for part, req in zip(parts, batch.requests): - logging.getLogger(LOGGER_NAME).debug('Batch handler is processing part %s for request %s', part, req) + logging.getLogger(LOGGER_NAME).debug( + "Batch handler is processing part %s for request %s", part, req + ) # if part represents multiple requests, dont' parse body and # process parts by appropriate reuqest instance @@ -1564,7 +1859,9 @@ def create_changeset(self, changeset_id=None): def changeset_handler(changeset, parts): """Gets changeset response from HTTP response""" - logging.getLogger(LOGGER_NAME).debug('Changeset handler called for changeset %s', changeset.id) + logging.getLogger(LOGGER_NAME).debug( + "Changeset handler called for changeset %s", changeset.id + ) result = [] @@ -1575,15 +1872,22 @@ def changeset_handler(changeset, parts): # raise error (even for successfull status codes) since such changeset response # always means something wrong happened on server response = ODataHttpResponse.from_string(parts[0]) - raise HttpError('Changeset cannot be processed due to single response received, status code: {}'.format( - response.status_code), response) + raise HttpError( + "Changeset cannot be processed due to single response received, status code: {}".format( + response.status_code + ), + response, + ) for part, req in zip(parts, changeset.requests): - logging.getLogger(LOGGER_NAME).debug('Changeset handler is processing part %s for request %s', part, - req) + logging.getLogger(LOGGER_NAME).debug( + "Changeset handler is processing part %s for request %s", part, req + ) if isinstance(req, MultipartRequest): - raise PyODataException('Changeset cannot contain nested multipart content') + raise PyODataException( + "Changeset cannot contain nested multipart content" + ) # part represents single request, we have to parse # content (without checking Content type for binary/http) @@ -1600,17 +1904,30 @@ class MultipartRequest(ODataHttpRequest): """HTTP Batch request""" def __init__(self, url, connection, handler, request_id=None): - super(MultipartRequest, self).__init__(url, connection, partial(MultipartRequest.http_response_handler, self)) + super(MultipartRequest, self).__init__( + url, connection, partial(MultipartRequest.http_response_handler, self) + ) self.requests = [] self._handler_decoded = handler # generate random id of form dddd-dddd-dddd # pylint: disable=invalid-name - self.id = request_id if request_id is not None else '{}_{}_{}'.format( - random.randint(1000, 9999), random.randint(1000, 9999), random.randint(1000, 9999)) + self.id = ( + request_id + if request_id is not None + else "{}_{}_{}".format( + random.randint(1000, 9999), + random.randint(1000, 9999), + random.randint(1000, 9999), + ) + ) - self._logger.debug('New multipart %s request initialized, id=%s', self.__class__.__name__, self.id) + self._logger.debug( + "New multipart %s request initialized, id=%s", + self.__class__.__name__, + self.id, + ) @property def handler(self): @@ -1622,7 +1939,10 @@ def get_boundary(self): def get_headers(self): # pylint: disable=no-self-use - return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary()), **self._headers} + return { + "Content-Type": "multipart/mixed;boundary={}".format(self.get_boundary()), + **self._headers, + } def get_body(self): return encode_multipart(self.get_boundary(), self.requests) @@ -1631,20 +1951,32 @@ def add_request(self, request): """Add request to be sent in batch""" self.requests.append(request) - self._logger.debug('New %s request added to multipart request %s', request.get_method(), self.id) + self._logger.debug( + "New %s request added to multipart request %s", + request.get_method(), + self.id, + ) @staticmethod def http_response_handler(request, response): """Process HTTP response to mutipart HTTP request""" if response.status_code != 202: # 202 Accepted - raise HttpError('HTTP POST for multipart request {0} failed with status code {1}' - .format(request.id, response.status_code), response) - - logging.getLogger(LOGGER_NAME).debug('Generic multipart http response request handler called') + raise HttpError( + "HTTP POST for multipart request {0} failed with status code {1}".format( + request.id, response.status_code + ), + response, + ) + + logging.getLogger(LOGGER_NAME).debug( + "Generic multipart http response request handler called" + ) # get list of all parts (headers + body) - decoded = decode_multipart(response.content.decode('utf-8'), response.headers['Content-Type']) + decoded = decode_multipart( + response.content.decode("utf-8"), response.headers["Content-Type"] + ) return request.handler(request, decoded) @@ -1653,19 +1985,20 @@ class BatchRequest(MultipartRequest): """HTTP Batch request""" def get_boundary(self): - return 'batch_' + self.id + return "batch_" + self.id def get_path(self): # pylint: disable=no-self-use - return '$batch' + return "$batch" def get_method(self): # pylint: disable=no-self-use - return 'POST' + return "POST" class Changeset(MultipartRequest): """Representation of changeset (unsorted group of requests)""" def get_boundary(self): - return 'changeset_' + self.id + return "changeset_" + self.id + From ff2288ac93cfaa3b1937bf9706f857a7de19e58f Mon Sep 17 00:00:00 2001 From: bartonip Date: Sun, 28 Jun 2020 09:01:38 +0000 Subject: [PATCH 09/10] Added tests to cover invalid lookup cases --- pyodata/v2/service.py | 4 +++- tests/test_service_v2.py | 25 +++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 4a215948..4cdb2aa2 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -1164,6 +1164,7 @@ class GetEntitySetFilterChainable: "range", "in", "length", + "eq" ] def __init__(self, request, filter_expressions, exprs): @@ -1221,7 +1222,8 @@ def decode_expression(self, expr, val): # field_heirarchy.append(part) elif part in self.__class__.operators: operator = part - + else: + raise ValueError("'{}' is not a valid property or operator".format(part)) # field = "/".join(field_heirarchy) # target_field = self.proprty_obj(field_heirarchy[-1]) diff --git a/tests/test_service_v2.py b/tests/test_service_v2.py index 152a9b59..0594d0da 100644 --- a/tests/test_service_v2.py +++ b/tests/test_service_v2.py @@ -1959,7 +1959,7 @@ def test_count_with_chainable_filter_or(service): assert request.execute() == 3 @responses.activate -def test_count_with_multiple_filters_startswith(service): +def test_count_with_multiple_chainable_filters_startswith(service): """Check getting $count with $filter calling startswith""" from pyodata.v2.service import FilterExpression as Q # pylint: disable=redefined-outer-name @@ -1978,9 +1978,30 @@ def test_count_with_multiple_filters_startswith(service): assert request.execute() == 3 +@responses.activate +def test_count_with_chainable_filters_invalid_property_lookup(service): + """Check getting $count with $filter calling startswith""" + # pylint: disable=redefined-outer-name + + employees = service.entity_sets.Employees.get_entities() + with pytest.raises(ValueError) as ex: + request = employees.filter(Foo="Bar") + + assert str(ex.value) == "'Foo' is not a valid property or operator" + + +@responses.activate +def test_count_with_chainable_filters_invalid_operator_lookup(service): + """Check getting $count with $filter calling startswith""" + # pylint: disable=redefined-outer-name + + employees = service.entity_sets.Employees.get_entities() + with pytest.raises(ValueError) as ex: + request = employees.filter(NickName__foo="Bar") + + assert str(ex.value) == "'foo' is not a valid property or operator" - @responses.activate def test_count_with_chained_filters(service): """Check getting $count with chained filters""" From f5000cc436e8f8566e3132cf5d92fd88b0a8515f Mon Sep 17 00:00:00 2001 From: bartonip Date: Sun, 28 Jun 2020 23:05:16 +0000 Subject: [PATCH 10/10] Fixed styling --- pyodata/v2/service.py | 833 ++++++++++++++---------------------------- 1 file changed, 272 insertions(+), 561 deletions(-) diff --git a/pyodata/v2/service.py b/pyodata/v2/service.py index 4cdb2aa2..463bfcc1 100644 --- a/pyodata/v2/service.py +++ b/pyodata/v2/service.py @@ -24,7 +24,7 @@ from pyodata.exceptions import HttpError, PyODataException, ExpressionError from . import model -LOGGER_NAME = "pyodata.service" +LOGGER_NAME = 'pyodata.service' HTTP_CODE_OK = 200 HTTP_CODE_CREATED = 201 @@ -33,7 +33,7 @@ def urljoin(*path): """Joins the passed string parts into a one string url""" - return "/".join((part.strip("/") for part in path)) + return '/'.join((part.strip('/') for part in path)) def encode_multipart(boundary, http_requests): @@ -41,40 +41,31 @@ def encode_multipart(boundary, http_requests): lines = [] - lines.append("") + lines.append('') for req in http_requests: - lines.append("--{0}".format(boundary)) + lines.append('--{0}'.format(boundary)) if not isinstance(req, MultipartRequest): - lines.extend( - ("Content-Type: application/http ", "Content-Transfer-Encoding:binary") - ) + lines.extend(('Content-Type: application/http ', 'Content-Transfer-Encoding:binary')) - lines.append("") + lines.append('') # request line (method + path + query params) - line = "{method} {path}".format( - method=req.get_method(), path=req.get_path() - ) - query_params = "&".join( - [ - "{}={}".format(key, val) - for key, val in req.get_query_params().items() - ] - ) + line = '{method} {path}'.format(method=req.get_method(), path=req.get_path()) + query_params = '&'.join(['{}={}'.format(key, val) for key, val in req.get_query_params().items()]) if query_params: - line += "?" + query_params - line += " HTTP/1.1" + line += '?' + query_params + line += ' HTTP/1.1' lines.append(line) # request specific headers for hdr, hdr_val in req.get_headers().items(): - lines.append("{}: {}".format(hdr, hdr_val)) + lines.append('{}: {}'.format(hdr, hdr_val)) - lines.append("") + lines.append('') body = req.get_body() if body is not None: @@ -83,11 +74,11 @@ def encode_multipart(boundary, http_requests): # this is very important since SAP gateway rejected request witout this line. It seems # blank line must be provided as a representation of emtpy body, else we are getting # 400 Bad fromat from SAP gateway - lines.append("") + lines.append('') - lines.append("--{0}--".format(boundary)) + lines.append('--{0}--'.format(boundary)) - return "\r\n".join(lines) + return '\r\n'.join(lines) def decode_multipart(data, content_type): @@ -98,7 +89,7 @@ def decode(message): messages = [] for i, part in enumerate(message.walk()): # pylint: disable=unused-variable - if part.get_content_type() == "multipart/mixed": + if part.get_content_type() == 'multipart/mixed': for submessage in part.get_payload(): messages.append(decode(submessage)) break @@ -132,7 +123,7 @@ class FakeSocket: """Fake socket to simulate received http response content""" def __init__(self, response_str): - self._file = BytesIO(response_str.encode("utf-8")) + self._file = BytesIO(response_str.encode('utf-8')) def makefile(self, *args, **kwargs): """Fake file that provides string content""" @@ -147,9 +138,7 @@ def makefile(self, *args, **kwargs): return ODataHttpResponse( response.getheaders(), response.status, - response.read( - len(data) - ), # the len here will give a 'big enough' value to read the whole content + response.read(len(data)) # the len here will give a 'big enough' value to read the whole content ) def json(self): @@ -159,7 +148,7 @@ def json(self): # approach can bring issues with encoding # https://github.com/requests/requests/blob/master/requests/models.py#L868 if self.content: - return json.loads(self.content.decode("utf-8")) + return json.loads(self.content.decode('utf-8')) return None @@ -191,15 +180,9 @@ def __init__(self, entity_type, single_key=None, **args): # check that entity type key consists of exactly one property if len(self._key) != 1: - raise PyODataException( - ( - "Key of entity type {} consists of multiple properties {} " - "and cannot be initialized by single value" - ).format( - self._entity_type.name, - ", ".join([prop.name for prop in self._key]), - ) - ) + raise PyODataException(('Key of entity type {} consists of multiple properties {} ' + 'and cannot be initialized by single value').format( + self._entity_type.name, ', '.join([prop.name for prop in self._key]))) # get single key property and format key string key_prop = self._key[0] @@ -207,20 +190,12 @@ def __init__(self, entity_type, single_key=None, **args): self._type = EntityKey.TYPE_SINGLE - self._logger.debug( - ( - "Detected single property key, adding pair %s->%s to key" - "properties" - ), - key_prop.name, - single_key, - ) + self._logger.debug(('Detected single property key, adding pair %s->%s to key' + 'properties'), key_prop.name, single_key) else: for key_prop in self._key: if key_prop.name not in args: - raise PyODataException( - "Missing value for key property {}".format(key_prop.name) - ) + raise PyODataException('Missing value for key property {}'.format(key_prop.name)) self._type = EntityKey.TYPE_COMPLEX @@ -244,17 +219,14 @@ def to_key_string_without_parentheses(self): # raise RuntimeError('Entity key is not complete, missing value of property: {0}'.format(key_prop.name)) key_pairs.append( - "{0}={1}".format( - key_prop.name, key_prop.to_literal(self._proprties[key_prop.name]) - ) - ) + '{0}={1}'.format(key_prop.name, key_prop.to_literal(self._proprties[key_prop.name]))) - return ",".join(key_pairs) + return ','.join(key_pairs) def to_key_string(self): """Gets the string representation of the key, including parentheses""" - return "({})".format(self.to_key_string_without_parentheses()) + return '({})'.format(self.to_key_string_without_parentheses()) def __repr__(self): return self.to_key_string() @@ -278,7 +250,7 @@ def handler(self): def get_path(self): """Get path of the HTTP request""" # pylint: disable=no-self-use - return "" + return '' def get_query_params(self): """Get query params""" @@ -288,20 +260,34 @@ def get_query_params(self): def get_method(self): """Get HTTP method""" # pylint: disable=no-self-use - return "GET" + return 'GET' def get_body(self): """Get HTTP body or None if not applicable""" # pylint: disable=no-self-use return None - def get_headers(self): - """Get dict of HTTP headers""" + def get_default_headers(self): + """Get dict of Child specific HTTP headers""" # pylint: disable=no-self-use - return None + return dict() + + def get_headers(self): + """Get dict of HTTP headers which is union of return value + of the method get_default_headers() and the headers + added via the method add_headers() where the latter + headers have priority - same keys get value of the latter. + """ + + headers = self.get_default_headers() + headers.update(self._headers) + + return headers def add_headers(self, value): - """Add HTTP headers to request""" + """Add the give dictionary of HTTP headers to + HTTP request sent by this ODataHttpRequest instance. + """ if not isinstance(value, dict): raise TypeError("Headers must be of type 'dict' not {}".format(type(value))) @@ -320,33 +306,27 @@ def execute(self): # pylint: disable=assignment-from-none body = self.get_body() - headers = self._headers - - # pylint: disable=assignment-from-none - extra_headers = self.get_headers() - if extra_headers is not None: - headers.update(extra_headers) + headers = self.get_headers() - self._logger.debug("Send (execute) %s request to %s", self.get_method(), url) - self._logger.debug(" query params: %s", self.get_query_params()) - self._logger.debug(" headers: %s", headers) + self._logger.debug('Send (execute) %s request to %s', self.get_method(), url) + self._logger.debug(' query params: %s', self.get_query_params()) + self._logger.debug(' headers: %s', headers) if body: - self._logger.debug(" body: %s", body) + self._logger.debug(' body: %s', body) params = "&".join("%s=%s" % (k, v) for k, v in self.get_query_params().items()) response = self._connection.request( - self.get_method(), url, headers=headers, params=params, data=body - ) + self.get_method(), url, headers=headers, params=params, data=body) - self._logger.debug("Received response") - self._logger.debug(" url: %s", response.url) - self._logger.debug(" headers: %s", response.headers) - self._logger.debug(" status code: %d", response.status_code) + self._logger.debug('Received response') + self._logger.debug(' url: %s', response.url) + self._logger.debug(' headers: %s', response.headers) + self._logger.debug(' status code: %d', response.status_code) try: - self._logger.debug(" body: %s", response.content.decode("utf-8")) + self._logger.debug(' body: %s', response.content.decode('utf-8')) except UnicodeDecodeError: - self._logger.debug(" body: ") + self._logger.debug(' body: ') return self._handler(response) @@ -355,19 +335,15 @@ class EntityGetRequest(ODataHttpRequest): """Used for GET operations of a single entity""" def __init__(self, handler, entity_key, entity_set_proxy): - super(EntityGetRequest, self).__init__( - entity_set_proxy.service.url, entity_set_proxy.service.connection, handler - ) + super(EntityGetRequest, self).__init__(entity_set_proxy.service.url, entity_set_proxy.service.connection, + handler) self._logger = logging.getLogger(LOGGER_NAME) self._entity_key = entity_key self._entity_set_proxy = entity_set_proxy self._select = None self._expand = None - self._logger.debug( - "New instance of EntityGetRequest for last segment: %s", - self._entity_set_proxy.last_segment, - ) + self._logger.debug('New instance of EntityGetRequest for last segment: %s', self._entity_set_proxy.last_segment) def nav(self, nav_property): """Navigates to given navigation property and returns the EntitySetProxy""" @@ -392,17 +368,17 @@ def expand(self, expand): 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", **self._headers} + def get_default_headers(self): + return {'Accept': 'application/json'} def get_query_params(self): qparams = super(EntityGetRequest, self).get_query_params() if self._select is not None: - qparams["$select"] = self._select + qparams['$select'] = self._select if self._expand is not None: - qparams["$expand"] = self._expand + qparams['$expand'] = self._expand return qparams @@ -416,18 +392,15 @@ def stream_handler(response): """Returns $value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for $value failed with status code {}".format( - response.status_code - ), - response, - ) + raise HttpError('HTTP GET for $value failed with status code {}' + .format(response.status_code), response) return response return ODataHttpRequest( - urljoin(self._url, self.get_path(), "/$value"), connection, stream_handler - ) + urljoin(self._url, self.get_path(), '/$value'), + connection, + stream_handler) class NavEntityGetRequest(EntityGetRequest): @@ -439,9 +412,7 @@ def __init__(self, handler, master_key, entity_set_proxy, nav_property): self._nav_property = nav_property def get_path(self): - return "{}/{}".format( - super(NavEntityGetRequest, self).get_path(), self._nav_property - ) + return "{}/{}".format(super(NavEntityGetRequest, self).get_path(), self._nav_property) class EntityCreateRequest(ODataHttpRequest): @@ -466,18 +437,15 @@ def __init__(self, url, connection, handler, entity_set, last_segment=None): # get all properties declared by entity type self._type_props = self._entity_type.proprties() - self._logger.debug( - "New instance of EntityCreateRequest for entity type: %s on path %s", - self._entity_type.name, - self._last_segment, - ) + self._logger.debug('New instance of EntityCreateRequest for entity type: %s on path %s', self._entity_type.name, + self._last_segment) def get_path(self): return self._last_segment def get_method(self): # pylint: disable=no-self-use - return "POST" + return 'POST' def _get_body(self): """Recursively builds a dictionary of values where some of the values @@ -497,13 +465,8 @@ def _get_body(self): 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", - **self._headers, - } + def get_default_headers(self): + return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X'} @staticmethod def _build_values(entity_type, entity): @@ -513,9 +476,7 @@ def _build_values(entity_type, entity): """ if isinstance(entity, list): - return [ - EntityCreateRequest._build_values(entity_type, item) for item in entity - ] + return [EntityCreateRequest._build_values(entity_type, item) for item in entity] values = {} for key, val in entity.items(): @@ -526,11 +487,8 @@ def _build_values(entity_type, entity): nav_prop = entity_type.nav_proprty(key) val = EntityCreateRequest._build_values(nav_prop.typ, val) except KeyError: - raise PyODataException( - "Property {} is not declared in {} entity type".format( - key, entity_type.name - ) - ) + raise PyODataException('Property {} is not declared in {} entity type'.format( + key, entity_type.name)) values[key] = val @@ -556,17 +514,14 @@ def __init__(self, url, connection, handler, entity_set, entity_key): self._entity_set = entity_set self._entity_key = entity_key - self._logger.debug( - "New instance of EntityDeleteRequest for entity type: %s", - entity_set.entity_type.name, - ) + self._logger.debug('New instance of EntityDeleteRequest for entity type: %s', entity_set.entity_type.name) def get_path(self): return self._entity_set.name + self._entity_key.to_key_string() def get_method(self): # pylint: disable=no-self-use - return "DELETE" + return 'DELETE' class EntityModifyRequest(ODataHttpRequest): @@ -575,11 +530,9 @@ class EntityModifyRequest(ODataHttpRequest): Call execute() to send the update-request to the OData service and get the modified entity.""" - ALLOWED_HTTP_METHODS = ["PATCH", "PUT", "MERGE"] + ALLOWED_HTTP_METHODS = ['PATCH', 'PUT', 'MERGE'] - def __init__( - self, url, connection, handler, entity_set, entity_key, method="PATCH" - ): + 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 @@ -588,21 +541,15 @@ def __init__( self._method = method.upper() if self._method not in EntityModifyRequest.ALLOWED_HTTP_METHODS: - raise ValueError( - 'The value "{}" is not on the list of allowed Entity Update HTTP Methods: {}'.format( - method, ", ".join(EntityModifyRequest.ALLOWED_HTTP_METHODS) - ) - ) + raise ValueError('The value "{}" is not on the list of allowed Entity Update HTTP Methods: {}' + .format(method, ', '.join(EntityModifyRequest.ALLOWED_HTTP_METHODS))) self._values = {} # get all properties declared by entity type self._type_props = self._entity_type.proprties() - self._logger.debug( - "New instance of EntityModifyRequest for entity type: %s", - self._entity_type.name, - ) + self._logger.debug('New instance of EntityModifyRequest for entity type: %s', self._entity_type.name) def get_path(self): return self._entity_set.name + self._entity_key.to_key_string() @@ -618,12 +565,8 @@ def get_body(self): body[key] = val return json.dumps(body) - def get_headers(self): - return { - "Accept": "application/json", - "Content-Type": "application/json", - **self._headers, - } + def get_default_headers(self): + return {'Accept': 'application/json', 'Content-Type': 'application/json'} def set(self, **kwargs): """Set properties to be changed.""" @@ -635,10 +578,7 @@ def set(self, **kwargs): val = self._entity_type.proprty(key).to_json(val) except KeyError: raise PyODataException( - "Property {} is not declared in {} entity type".format( - key, self._entity_type.name - ) - ) + 'Property {} is not declared in {} entity type'.format(key, self._entity_type.name)) self._values[key] = val @@ -663,9 +603,7 @@ def __init__(self, url, connection, handler, last_segment): self._expand = None self._last_segment = last_segment self._customs = {} # string -> string hash - self._logger.debug( - "New instance of QueryRequest for last segment: %s", self._last_segment - ) + self._logger.debug('New instance of QueryRequest for last segment: %s', self._last_segment) def custom(self, name, value): """Adds a custom name-value pair.""" @@ -686,10 +624,10 @@ def expand(self, expand): def filter(self, filter_val): """Sets the filter expression.""" # returns QueryRequest - self._filter = filter_val + self._filter = quote(filter_val) return self - # def nav(self, key_value, nav_property): + # def nav(self, key_value, nav_property):æ # """Navigates to a referenced collection using a collection-valued navigation property.""" # # returns QueryRequest # raise NotImplementedError @@ -716,39 +654,41 @@ def top(self, top): def get_path(self): if self._count: - return urljoin(self._last_segment, "/$count") + return urljoin(self._last_segment, '/$count') return self._last_segment - def get_headers(self): + def get_default_headers(self): if self._count: return {} - return {"Accept": "application/json", **self._headers} + return { + 'Accept': 'application/json', + } def get_query_params(self): qparams = super(QueryRequest, self).get_query_params() if self._top is not None: - qparams["$top"] = self._top + qparams['$top'] = self._top if self._skip is not None: - qparams["$skip"] = self._skip + qparams['$skip'] = self._skip if self._order_by is not None: - qparams["$orderby"] = self._order_by + qparams['$orderby'] = self._order_by if self._filter is not None: - qparams["$filter"] = self._filter + qparams['$filter'] = self._filter if self._select is not None: - qparams["$select"] = self._select + qparams['$select'] = self._select for key, val in self._customs.items(): qparams[key] = val if self._expand is not None: - qparams["$expand"] = self._expand + qparams['$expand'] = self._expand return qparams @@ -757,18 +697,14 @@ class FunctionRequest(QueryRequest): """Function import request (Service call)""" def __init__(self, url, connection, handler, function_import): - super(FunctionRequest, self).__init__( - url, connection, handler, function_import.name - ) + super(FunctionRequest, self).__init__(url, connection, handler, function_import.name) self._function_import = function_import - self._logger.debug( - "New instance of FunctionRequest for %s", self._function_import.name - ) + self._logger.debug('New instance of FunctionRequest for %s', self._function_import.name) def parameter(self, name, value): - """Sets value of parameter.""" + '''Sets value of parameter.''' # check if param is valid (is declared in metadata) try: @@ -777,19 +713,18 @@ def parameter(self, name, value): # add parameter as custom query argument self.custom(param.name, param.to_literal(value)) except KeyError: - raise PyODataException( - "Function import {0} does not have pararmeter {1}".format( - self._function_import.name, name - ) - ) + raise PyODataException('Function import {0} does not have pararmeter {1}' + .format(self._function_import.name, name)) return self def get_method(self): return self._function_import.http_method - def get_headers(self): - return {"Accept": "application/json", **self._headers} + def get_default_headers(self): + return { + 'Accept': 'application/json' + } class EntityProxy: @@ -800,9 +735,7 @@ class EntityProxy: # pylint: disable=too-many-branches,too-many-nested-blocks - def __init__( - self, service, entity_set, entity_type, proprties=None, entity_key=None - ): + def __init__(self, service, entity_set, entity_type, proprties=None, entity_key=None): self._logger = logging.getLogger(LOGGER_NAME) self._service = service self._entity_set = entity_set @@ -811,11 +744,7 @@ def __init__( self._cache = dict() self._entity_key = entity_key - self._logger.debug( - "New entity proxy instance of type %s from properties: %s", - entity_type.name, - proprties, - ) + self._logger.debug('New entity proxy instance of type %s from properties: %s', entity_type.name, proprties) # cache values of individual properties if provided if proprties is not None: @@ -824,14 +753,10 @@ def __init__( for type_proprty in self._entity_type.proprties(): if type_proprty.name in proprties: if proprties[type_proprty.name] is not None: - self._cache[type_proprty.name] = type_proprty.from_json( - proprties[type_proprty.name] - ) + self._cache[type_proprty.name] = type_proprty.from_json(proprties[type_proprty.name]) else: # null value is in literal form for now, convert it to python representation - self._cache[type_proprty.name] = type_proprty.from_literal( - type_proprty.typ.null_value - ) + self._cache[type_proprty.name] = type_proprty.from_literal(type_proprty.typ.null_value) # then, assign all navigation properties for prop in self._entity_type.nav_proprties: @@ -842,41 +767,30 @@ def __init__( prop_etype = prop.to_role.entity_type # cache value according to multiplicity - if prop.to_role.multiplicity in [ - model.EndRole.MULTIPLICITY_ONE, - model.EndRole.MULTIPLICITY_ZERO_OR_ONE, - ]: + if prop.to_role.multiplicity in \ + [model.EndRole.MULTIPLICITY_ONE, + model.EndRole.MULTIPLICITY_ZERO_OR_ONE]: # cache None in case we receive nothing (null) instead of entity data if proprties[prop.name] is None: self._cache[prop.name] = None else: - self._cache[prop.name] = EntityProxy( - service, None, prop_etype, proprties[prop.name] - ) - - elif ( - prop.to_role.multiplicity - == model.EndRole.MULTIPLICITY_ZERO_OR_MORE - ): + self._cache[prop.name] = EntityProxy(service, None, prop_etype, proprties[prop.name]) + + elif prop.to_role.multiplicity == model.EndRole.MULTIPLICITY_ZERO_OR_MORE: # default value is empty array self._cache[prop.name] = [] # if there are no entities available, received data consists of # metadata properties only. - if "results" in proprties[prop.name]: + if 'results' in proprties[prop.name]: # available entities are serialized in results array - for entity in proprties[prop.name]["results"]: - self._cache[prop.name].append( - EntityProxy(service, None, prop_etype, entity) - ) + for entity in proprties[prop.name]['results']: + self._cache[prop.name].append(EntityProxy(service, None, prop_etype, entity)) else: - raise PyODataException( - "Unknown multiplicity {0} of association role {1}".format( - prop.to_role.multiplicity, prop.to_role.name - ) - ) + raise PyODataException('Unknown multiplicity {0} of association role {1}' + .format(prop.to_role.multiplicity, prop.to_role.name)) # build entity key if not provided if self._entity_key is None: @@ -884,9 +798,7 @@ def __init__( try: # if key seems to be simple (consists of single property) if len(self._key_props) == 1: - self._entity_key = EntityKey( - entity_type, self._cache[self._key_props[0].name] - ) + self._entity_key = EntityKey(entity_type, self._cache[self._key_props[0].name]) else: # build complex key self._entity_key = EntityKey(entity_type, **self._cache) @@ -907,11 +819,8 @@ def __getattr__(self, attr): self._cache[attr] = value return value except KeyError as ex: - raise AttributeError( - "EntityType {0} does not have Property {1}: {2}".format( - self._entity_type.name, attr, str(ex) - ) - ) + raise AttributeError('EntityType {0} does not have Property {1}: {2}' + .format(self._entity_type.name, attr, str(ex))) def nav(self, nav_property): """Navigates to given navigation property and returns the EntitySetProxy""" @@ -920,83 +829,58 @@ def nav(self, nav_property): try: navigation_property = self._entity_type.nav_proprty(nav_property) except KeyError: - raise PyODataException( - "Navigation property {} is not declared in {} entity type".format( - nav_property, self._entity_type - ) - ) + raise PyODataException('Navigation property {} is not declared in {} entity type'.format( + nav_property, self._entity_type)) # Get entity set of navigation property association_info = navigation_property.association_info association_set = self._service.schema.association_set_by_association( - association_info.name, association_info.namespace - ) + association_info.name, + association_info.namespace) navigation_entity_set = None for end in association_set.end_roles: - if ( - association_set.end_by_entity_set(end.entity_set_name).role - == navigation_property.to_role.role - ): - navigation_entity_set = self._service.schema.entity_set( - end.entity_set_name, association_info.namespace - ) + if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role: + navigation_entity_set = self._service.schema.entity_set(end.entity_set_name, association_info.namespace) if not navigation_entity_set: - raise PyODataException( - "No association set for role {}".format(navigation_property.to_role) - ) + raise PyODataException('No association set for role {}'.format(navigation_property.to_role)) roles = navigation_property.association.end_roles - if all( - ( - role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE - for role in roles - ) - ): - return NavEntityProxy( - self, nav_property, navigation_entity_set.entity_type, {} - ) + if all((role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE for role in roles)): + return NavEntityProxy(self, nav_property, navigation_entity_set.entity_type, {}) return EntitySetProxy( self._service, self._service.schema.entity_set(navigation_entity_set.name), nav_property, - self._entity_set.name + self._entity_key.to_key_string(), - ) + self._entity_set.name + self._entity_key.to_key_string()) def get_path(self): """Returns this entity's relative path - e.g. EntitySet(KEY)""" - return ( - self._entity_set._name + self._entity_key.to_key_string() - ) # pylint: disable=protected-access + return self._entity_set._name + self._entity_key.to_key_string() # pylint: disable=protected-access def get_proprty(self, name, connection=None): """Returns value of the property""" - self._logger.info("Initiating property request for %s", name) + self._logger.info('Initiating property request for %s', name) def proprty_get_handler(key, proprty, response): """Gets property value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for Attribute {0} of Entity {1} failed with status code {2}".format( - proprty.name, key, response.status_code - ), - response, - ) + raise HttpError('HTTP GET for Attribute {0} of Entity {1} failed with status code {2}' + .format(proprty.name, key, response.status_code), response) - data = response.json()["d"] + data = response.json()['d'] return proprty.from_json(data[proprty.name]) path = urljoin(self.get_path(), name) return self._service.http_get_odata( path, partial(proprty_get_handler, path, self._entity_type.proprty(name)), - connection=connection, - ) + connection=connection) def get_value(self, connection=None): "Returns $value of Stream entities" @@ -1005,19 +889,15 @@ def value_get_handler(key, response): """Gets property value from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for $value of Entity {0} failed with status code {1}".format( - key, response.status_code - ), - response, - ) + raise HttpError('HTTP GET for $value of Entity {0} failed with status code {1}' + .format(key, response.status_code), response) return response - path = urljoin(self.get_path(), "/$value") - return self._service.http_get_odata( - path, partial(value_get_handler, self.entity_key), connection=connection - ) + path = urljoin(self.get_path(), '/$value') + return self._service.http_get_odata(path, + partial(value_get_handler, self.entity_key), + connection=connection) @property def entity_set(self): @@ -1035,7 +915,7 @@ def entity_key(self): def url(self): """URL of the real entity""" - service_url = self._service.url.rstrip("/") + service_url = self._service.url.rstrip('/') entity_path = self.get_path() return urljoin(service_url, entity_path) @@ -1051,9 +931,7 @@ class NavEntityProxy(EntityProxy): def __init__(self, parent_entity, prop_name, entity_type, entity): # pylint: disable=protected-access - super(NavEntityProxy, self).__init__( - parent_entity._service, parent_entity._entity_set, entity_type, entity - ) + super(NavEntityProxy, self).__init__(parent_entity._service, parent_entity._entity_set, entity_type, entity) self._parent_entity = parent_entity self._prop_name = prop_name @@ -1075,47 +953,45 @@ def build_expression(operator, operands): """Creates a expression by joining the operands with the operator""" if len(operands) < 2: - raise ExpressionError( - "The $filter operator '{}' needs at least two operands".format(operator) - ) + raise ExpressionError('The $filter operator \'{}\' needs at least two operands'.format(operator)) - return "({})".format(" {} ".format(operator).join(operands)) + return '({})'.format(' {} '.format(operator).join(operands)) @staticmethod def and_(*operands): """Creates logical AND expression from the operands""" - return GetEntitySetFilter.build_expression("and", operands) + return GetEntitySetFilter.build_expression('and', operands) @staticmethod def or_(*operands): """Creates logical OR expression from the operands""" - return GetEntitySetFilter.build_expression("or", operands) + return GetEntitySetFilter.build_expression('or', operands) @staticmethod def format_filter(proprty, operator, value): """Creates a filter expression """ - return "{} {} {}".format(proprty.name, operator, proprty.to_literal(value)) + return '{} {} {}'.format(proprty.name, operator, proprty.to_literal(value)) def __eq__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "eq", value) + return GetEntitySetFilter.format_filter(self._proprty, 'eq', value) def __ne__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "ne", value) + return GetEntitySetFilter.format_filter(self._proprty, 'ne', value) def __lt__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "lt", value) + return GetEntitySetFilter.format_filter(self._proprty, 'lt', value) def __le__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "le", value) + return GetEntitySetFilter.format_filter(self._proprty, 'le', value) def __ge__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "ge", value) + return GetEntitySetFilter.format_filter(self._proprty, 'ge', value) def __gt__(self, value): - return GetEntitySetFilter.format_filter(self._proprty, "gt", value) + return GetEntitySetFilter.format_filter(self._proprty, 'gt', value) class FilterExpression: @@ -1174,7 +1050,7 @@ def __init__(self, request, filter_expressions, exprs): def proprty_obj(self, name): """Returns value for a particular proprty""" - return self.request._entity_type.proprty(name) # pylint: disable=protected-access + return self.request._entity_type.proprty(name) # pylint: disable=protected-access def process_query_objects(self): """Processes FilterExpression objects to OData lookups""" @@ -1210,7 +1086,7 @@ def process_expressions(self): def decode_expression(self, expr, val): """Decodes Django-like syntax into OData expressions""" - properties = self.request._entity_type._properties.keys() # pylint: disable=protected-access + properties = self.request._entity_type._properties.keys() # pylint: disable=protected-access field = None # field_heirarchy = [] operator = "eq" @@ -1292,9 +1168,7 @@ class GetEntitySetRequest(QueryRequest): """GET on EntitySet""" def __init__(self, url, connection, handler, last_segment, entity_type): - super(GetEntitySetRequest, self).__init__( - url, connection, handler, last_segment - ) + super(GetEntitySetRequest, self).__init__(url, connection, handler, last_segment) self._entity_type = entity_type @@ -1333,17 +1207,17 @@ def __init__(self, service, entity_set, alias=None, parent_last_segment=None): self._entity_set = entity_set self._alias = alias if parent_last_segment is None: - self._parent_last_segment = "" + self._parent_last_segment = '' else: - if parent_last_segment.endswith("/"): + if parent_last_segment.endswith('/'): self._parent_last_segment = parent_last_segment else: - self._parent_last_segment = parent_last_segment + "/" + self._parent_last_segment = parent_last_segment + '/' self._name = entity_set.name self._key = entity_set.entity_type.key_proprties self._logger = logging.getLogger(LOGGER_NAME) - self._logger.debug("New entity set proxy instance for %s", self._name) + self._logger.debug('New entity set proxy instance for %s', self._name) @property def service(self): @@ -1354,9 +1228,7 @@ def service(self): def last_segment(self): """Return last segment of url""" - entity_set_name = ( - self._alias if self._alias is not None else self._entity_set.name - ) + entity_set_name = self._alias if self._alias is not None else self._entity_set.name return self._parent_last_segment + entity_set_name def nav(self, nav_property, key): @@ -1365,50 +1237,32 @@ def nav(self, nav_property, key): try: navigation_property = self._entity_set.entity_type.nav_proprty(nav_property) except KeyError: - raise PyODataException( - "Navigation property {} is not declared in {} entity type".format( - nav_property, self._entity_set.entity_type - ) - ) + raise PyODataException('Navigation property {} is not declared in {} entity type'.format( + nav_property, self._entity_set.entity_type)) # Get entity set of navigation property association_info = navigation_property.association_info association_set = self._service.schema.association_set_by_association( - association_info.name - ) + association_info.name) navigation_entity_set = None for end in association_set.end_roles: - if ( - association_set.end_by_entity_set(end.entity_set_name).role - == navigation_property.to_role.role - ): - navigation_entity_set = self._service.schema.entity_set( - end.entity_set_name - ) + if association_set.end_by_entity_set(end.entity_set_name).role == navigation_property.to_role.role: + navigation_entity_set = self._service.schema.entity_set(end.entity_set_name) if not navigation_entity_set: raise PyODataException( - "No association set for role {} {}".format( - navigation_property.to_role, association_set.end_roles - ) - ) + 'No association set for role {} {}'.format(navigation_property.to_role, association_set.end_roles)) roles = navigation_property.association.end_roles - if all( - ( - role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE - for role in roles - ) - ): + if all((role.multiplicity != model.EndRole.MULTIPLICITY_ZERO_OR_MORE for role in roles)): return self._get_nav_entity(key, nav_property, navigation_entity_set) return EntitySetProxy( self._service, navigation_entity_set, nav_property, - self._entity_set.name + key.to_key_string(), - ) + self._entity_set.name + key.to_key_string()) def _get_nav_entity(self, master_key, nav_property, navigation_entity_set): """Get entity based on provided key of the master and Navigation property name""" @@ -1417,36 +1271,26 @@ def get_entity_handler(parent, nav_property, navigation_entity_set, response): """Gets entity from HTTP response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for Entity {0} failed with status code {1}".format( - self._name, response.status_code - ), - response, - ) + raise HttpError('HTTP GET for Entity {0} failed with status code {1}' + .format(self._name, response.status_code), response) - entity = response.json()["d"] + entity = response.json()['d'] - return NavEntityProxy( - parent, nav_property, navigation_entity_set.entity_type, entity - ) + return NavEntityProxy(parent, nav_property, navigation_entity_set.entity_type, entity) self._logger.info( - "Getting the nav property %s of the entity %s for the key %s", + 'Getting the nav property %s of the entity %s for the key %s', nav_property, self._entity_set.entity_type.name, - master_key, - ) + master_key) - parent = EntityProxy( - self._service, self, self._entity_set.entity_type, entity_key=master_key - ) + parent = EntityProxy(self._service, self, self._entity_set.entity_type, entity_key=master_key) return NavEntityGetRequest( partial(get_entity_handler, parent, nav_property, navigation_entity_set), master_key, self, - nav_property, - ) + nav_property) def get_entity(self, key=None, **args): """Get entity based on provided key properties""" @@ -1455,30 +1299,19 @@ def get_entity_handler(response): """Gets entity from HTTP response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for Entity {0} failed with status code {1}".format( - self._name, response.status_code - ), - response, - ) + raise HttpError('HTTP GET for Entity {0} failed with status code {1}' + .format(self._name, response.status_code), response) - entity = response.json()["d"] + entity = response.json()['d'] - return EntityProxy( - self._service, self._entity_set, self._entity_set.entity_type, entity - ) + return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **args) - self._logger.info( - "Getting entity %s for key %s and args %s", - self._entity_set.entity_type.name, - key, - args, - ) + self._logger.info('Getting entity %s for key %s and args %s', self._entity_set.entity_type.name, key, args) return EntityGetRequest(get_entity_handler, entity_key, self) @@ -1489,43 +1322,30 @@ def get_entities_handler(response): """Gets entity set from HTTP Response""" if response.status_code != HTTP_CODE_OK: - raise HttpError( - "HTTP GET for Entity Set {0} failed with status code {1}".format( - self._name, response.status_code - ), - response, - ) + raise HttpError('HTTP GET for Entity Set {0} failed with status code {1}' + .format(self._name, response.status_code), response) content = response.json() if isinstance(content, int): return content - entities = content["d"] + entities = content['d'] if isinstance(entities, dict): - entities = entities["results"] + entities = entities['results'] - self._logger.info("Fetched %d entities", len(entities)) + self._logger.info('Fetched %d entities', len(entities)) result = [] for props in entities: - entity = EntityProxy( - self._service, self._entity_set, self._entity_set.entity_type, props - ) + entity = EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, props) result.append(entity) return result - entity_set_name = ( - self._alias if self._alias is not None else self._entity_set.name - ) - return GetEntitySetRequest( - self._service.url, - self._service.connection, - get_entities_handler, - self._parent_last_segment + entity_set_name, - self._entity_set.entity_type, - ) + entity_set_name = self._alias if self._alias is not None else self._entity_set.name + return GetEntitySetRequest(self._service.url, self._service.connection, get_entities_handler, + self._parent_last_segment + entity_set_name, self._entity_set.entity_type) def create_entity(self, return_code=HTTP_CODE_CREATED): """Creates a new entity in the given entity-set.""" @@ -1534,29 +1354,15 @@ def create_entity_handler(response): """Gets newly created entity encoded in HTTP Response""" if response.status_code != return_code: - raise HttpError( - "HTTP POST for Entity Set {0} failed with status code {1}".format( - self._name, response.status_code - ), - response, - ) + raise HttpError('HTTP POST for Entity Set {0} failed with status code {1}' + .format(self._name, response.status_code), response) - entity_props = response.json()["d"] + entity_props = response.json()['d'] - return EntityProxy( - self._service, - self._entity_set, - self._entity_set.entity_type, - entity_props, - ) + return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity_props) - return EntityCreateRequest( - self._service.url, - self._service.connection, - create_entity_handler, - self._entity_set, - self.last_segment, - ) + return EntityCreateRequest(self._service.url, self._service.connection, create_entity_handler, self._entity_set, + self.last_segment) def update_entity(self, key=None, method=None, **kwargs): """Updates an existing entity in the given entity-set.""" @@ -1565,36 +1371,21 @@ def update_entity_handler(response): """Gets modified entity encoded in HTTP Response""" if response.status_code != 204: - raise HttpError( - "HTTP modify request for Entity Set {} failed with status code {}".format( - self._name, response.status_code - ), - response, - ) + raise HttpError('HTTP modify request for Entity Set {} failed with status code {}' + .format(self._name, response.status_code), response) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **kwargs) - self._logger.info( - "Updating entity %s for key %s and args %s", - self._entity_set.entity_type.name, - key, - kwargs, - ) + self._logger.info('Updating entity %s for key %s and args %s', self._entity_set.entity_type.name, key, kwargs) if method is None: - method = self._service.config["http"]["update_method"] - - return EntityModifyRequest( - self._service.url, - self._service.connection, - update_entity_handler, - self._entity_set, - entity_key, - method=method, - ) + method = self._service.config['http']['update_method'] + + return EntityModifyRequest(self._service.url, self._service.connection, update_entity_handler, self._entity_set, + entity_key, method=method) def delete_entity(self, key: EntityKey = None, **kwargs): """Delete the entity""" @@ -1603,24 +1394,17 @@ def delete_entity_handler(response): """Check if entity deletion was successful""" if response.status_code != 204: - raise HttpError( - f"HTTP POST for Entity delete {self._name} " - f"failed with status code {response.status_code}", - response, - ) + raise HttpError(f'HTTP POST for Entity delete {self._name} ' + f'failed with status code {response.status_code}', + response) if key is not None and isinstance(key, EntityKey): entity_key = key else: entity_key = EntityKey(self._entity_set.entity_type, key, **kwargs) - return EntityDeleteRequest( - self._service.url, - self._service.connection, - delete_entity_handler, - self._entity_set, - entity_key, - ) + return EntityDeleteRequest(self._service.url, self._service.connection, delete_entity_handler, self._entity_set, + entity_key) # pylint: disable=too-few-public-methods @@ -1633,19 +1417,14 @@ def __init__(self, service): self._entity_sets = dict() for entity_set in self._service.schema.entity_sets: - self._entity_sets[entity_set.name] = EntitySetProxy( - self._service, entity_set - ) + self._entity_sets[entity_set.name] = EntitySetProxy(self._service, entity_set) def __getattr__(self, name): try: return self._entity_sets[name] except KeyError: raise AttributeError( - "EntitySet {0} not defined in {1}.".format( - name, ",".join(list(self._entity_sets.keys())) - ) - ) + 'EntitySet {0} not defined in {1}.'.format(name, ','.join(list(self._entity_sets.keys())))) class FunctionContainer: @@ -1666,10 +1445,7 @@ def __getattr__(self, name): if name not in self._functions: raise AttributeError( - "Function {0} not defined in {1}.".format( - name, ",".join(list(self._functions.keys())) - ) - ) + 'Function {0} not defined in {1}.'.format(name, ','.join(list(self._functions.keys())))) fimport = self._service.schema.function_import(name) @@ -1677,82 +1453,60 @@ def function_import_handler(fimport, response): """Get function call response from HTTP Response""" if 300 <= response.status_code < 400: - raise HttpError( - f"Function Import {fimport.name} requires Redirection which is not supported", - response, - ) + raise HttpError(f'Function Import {fimport.name} requires Redirection which is not supported', + response) if response.status_code == 401: - raise HttpError( - f"Not authorized to call Function Import {fimport.name}", response - ) + raise HttpError(f'Not authorized to call Function Import {fimport.name}', + response) if response.status_code == 403: - raise HttpError( - f"Missing privileges to call Function Import {fimport.name}", - response, - ) + raise HttpError(f'Missing privileges to call Function Import {fimport.name}', + response) if response.status_code == 405: raise HttpError( - f"Despite definition Function Import {fimport.name} does not support HTTP {fimport.http_method}", - response, - ) + f'Despite definition Function Import {fimport.name} does not support HTTP {fimport.http_method}', + response) if 400 <= response.status_code < 500: raise HttpError( - f"Function Import {fimport.name} call has failed with status code {response.status_code}", - response, - ) + f'Function Import {fimport.name} call has failed with status code {response.status_code}', + response) if response.status_code >= 500: - raise HttpError( - f"Server has encountered an error while processing Function Import {fimport.name}", - response, - ) + raise HttpError(f'Server has encountered an error while processing Function Import {fimport.name}', + response) if fimport.return_type is None: if response.status_code != 204: logging.getLogger(LOGGER_NAME).warning( - "The No Return Function Import %s has replied with HTTP Status Code %d instead of 204", - fimport.name, - response.status_code, - ) + 'The No Return Function Import %s has replied with HTTP Status Code %d instead of 204', + fimport.name, response.status_code) if response.text: logging.getLogger(LOGGER_NAME).warning( - "The No Return Function Import %s has returned content:\n%s", - fimport.name, - response.text, - ) + 'The No Return Function Import %s has returned content:\n%s', fimport.name, response.text) return None if response.status_code != 200: logging.getLogger(LOGGER_NAME).warning( - "The Function Import %s has replied with HTTP Status Code %d instead of 200", - fimport.name, - response.status_code, - ) + 'The Function Import %s has replied with HTTP Status Code %d instead of 200', + fimport.name, response.status_code) - response_data = response.json()["d"] + response_data = response.json()['d'] # 1. if return types is "entity type", return instance of appropriate entity proxy if isinstance(fimport.return_type, model.EntityType): entity_set = self._service.schema.entity_set(fimport.entity_set_name) - return EntityProxy( - self._service, entity_set, fimport.return_type, response_data - ) + return EntityProxy(self._service, entity_set, fimport.return_type, response_data) # 2. return raw data for all other return types (primitives, complex types encoded in dicts, etc.) return response_data - return FunctionRequest( - self._service.url, - self._service.connection, - partial(function_import_handler, fimport), - fimport, - ) + return FunctionRequest(self._service.url, self._service.connection, + partial(function_import_handler, fimport), fimport) class Service: @@ -1765,7 +1519,7 @@ def __init__(self, url, schema, connection): self._entity_container = EntityContainer(self) self._function_container = FunctionContainer(self) - self._config = {"http": {"update_method": "PATCH"}} + self._config = {'http': {'update_method': 'PATCH'}} @property def schema(self): @@ -1823,8 +1577,7 @@ def http_get_odata(self, path, handler, connection=None): urljoin(self._url, path), conn, handler, - headers={"Accept": "application/json"}, - ) + headers={'Accept': 'application/json'}) def create_batch(self, batch_id=None): """Create instance of OData batch request""" @@ -1832,15 +1585,11 @@ def create_batch(self, batch_id=None): def batch_handler(batch, parts): """Process parsed multipart request (parts)""" - logging.getLogger(LOGGER_NAME).debug( - "Batch handler called for batch %s", batch.id - ) + logging.getLogger(LOGGER_NAME).debug('Batch handler called for batch %s', batch.id) result = [] for part, req in zip(parts, batch.requests): - logging.getLogger(LOGGER_NAME).debug( - "Batch handler is processing part %s for request %s", part, req - ) + logging.getLogger(LOGGER_NAME).debug('Batch handler is processing part %s for request %s', part, req) # if part represents multiple requests, dont' parse body and # process parts by appropriate reuqest instance @@ -1861,9 +1610,7 @@ def create_changeset(self, changeset_id=None): def changeset_handler(changeset, parts): """Gets changeset response from HTTP response""" - logging.getLogger(LOGGER_NAME).debug( - "Changeset handler called for changeset %s", changeset.id - ) + logging.getLogger(LOGGER_NAME).debug('Changeset handler called for changeset %s', changeset.id) result = [] @@ -1874,22 +1621,15 @@ def changeset_handler(changeset, parts): # raise error (even for successfull status codes) since such changeset response # always means something wrong happened on server response = ODataHttpResponse.from_string(parts[0]) - raise HttpError( - "Changeset cannot be processed due to single response received, status code: {}".format( - response.status_code - ), - response, - ) + raise HttpError('Changeset cannot be processed due to single response received, status code: {}'.format( + response.status_code), response) for part, req in zip(parts, changeset.requests): - logging.getLogger(LOGGER_NAME).debug( - "Changeset handler is processing part %s for request %s", part, req - ) + logging.getLogger(LOGGER_NAME).debug('Changeset handler is processing part %s for request %s', part, + req) if isinstance(req, MultipartRequest): - raise PyODataException( - "Changeset cannot contain nested multipart content" - ) + raise PyODataException('Changeset cannot contain nested multipart content') # part represents single request, we have to parse # content (without checking Content type for binary/http) @@ -1906,30 +1646,17 @@ class MultipartRequest(ODataHttpRequest): """HTTP Batch request""" def __init__(self, url, connection, handler, request_id=None): - super(MultipartRequest, self).__init__( - url, connection, partial(MultipartRequest.http_response_handler, self) - ) + super(MultipartRequest, self).__init__(url, connection, partial(MultipartRequest.http_response_handler, self)) self.requests = [] self._handler_decoded = handler # generate random id of form dddd-dddd-dddd # pylint: disable=invalid-name - self.id = ( - request_id - if request_id is not None - else "{}_{}_{}".format( - random.randint(1000, 9999), - random.randint(1000, 9999), - random.randint(1000, 9999), - ) - ) + self.id = request_id if request_id is not None else '{}_{}_{}'.format( + random.randint(1000, 9999), random.randint(1000, 9999), random.randint(1000, 9999)) - self._logger.debug( - "New multipart %s request initialized, id=%s", - self.__class__.__name__, - self.id, - ) + self._logger.debug('New multipart %s request initialized, id=%s', self.__class__.__name__, self.id) @property def handler(self): @@ -1939,12 +1666,9 @@ def get_boundary(self): """Get boundary used for request parts""" return self.id - def get_headers(self): + def get_default_headers(self): # pylint: disable=no-self-use - return { - "Content-Type": "multipart/mixed;boundary={}".format(self.get_boundary()), - **self._headers, - } + return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary())} def get_body(self): return encode_multipart(self.get_boundary(), self.requests) @@ -1953,32 +1677,20 @@ def add_request(self, request): """Add request to be sent in batch""" self.requests.append(request) - self._logger.debug( - "New %s request added to multipart request %s", - request.get_method(), - self.id, - ) + self._logger.debug('New %s request added to multipart request %s', request.get_method(), self.id) @staticmethod def http_response_handler(request, response): """Process HTTP response to mutipart HTTP request""" if response.status_code != 202: # 202 Accepted - raise HttpError( - "HTTP POST for multipart request {0} failed with status code {1}".format( - request.id, response.status_code - ), - response, - ) + raise HttpError('HTTP POST for multipart request {0} failed with status code {1}' + .format(request.id, response.status_code), response) - logging.getLogger(LOGGER_NAME).debug( - "Generic multipart http response request handler called" - ) + logging.getLogger(LOGGER_NAME).debug('Generic multipart http response request handler called') # get list of all parts (headers + body) - decoded = decode_multipart( - response.content.decode("utf-8"), response.headers["Content-Type"] - ) + decoded = decode_multipart(response.content.decode('utf-8'), response.headers['Content-Type']) return request.handler(request, decoded) @@ -1987,20 +1699,19 @@ class BatchRequest(MultipartRequest): """HTTP Batch request""" def get_boundary(self): - return "batch_" + self.id + return 'batch_' + self.id def get_path(self): # pylint: disable=no-self-use - return "$batch" + return '$batch' def get_method(self): # pylint: disable=no-self-use - return "POST" + return 'POST' class Changeset(MultipartRequest): """Representation of changeset (unsorted group of requests)""" def get_boundary(self): - return "changeset_" + self.id - + return 'changeset_' + self.id