Skip to content

A few improvements #111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry but I cannot see any code for this last changelog entry.


## [1.5.0]

Expand Down
45 changes: 35 additions & 10 deletions pyodata/v2/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ def __init__(self, url, connection, handler, headers=None):
self._connection = connection
self._url = url
self._handler = handler
self._headers = headers
self._headers = headers or dict()
self._logger = logging.getLogger(LOGGER_NAME)

@property
Expand Down Expand Up @@ -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

Expand All @@ -284,7 +290,7 @@ def execute(self):
# pylint: disable=assignment-from-none
body = self.get_body()

headers = {} if self._headers is None else self._headers
headers = self._headers

# pylint: disable=assignment-from-none
extra_headers = self.get_headers()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you keep the get_headers method as it is this patch, then we don't need the line 287 at all. But I would rather keep the line 287 and remove all occurences of slef.headers from the redefined get_headers. I don't think its a good idea to force descendant classes to use the headers class member to return the correct headers - this is up to the parent class.

Expand Down Expand Up @@ -351,7 +357,7 @@ def get_path(self):
return self._entity_set_proxy.last_segment + self._entity_key.to_key_string()

def get_headers(self):
return {'Accept': 'application/json'}
return {'Accept': 'application/json', **self._headers}

def get_query_params(self):
qparams = super(EntityGetRequest, self).get_query_params()
Expand Down Expand Up @@ -448,7 +454,7 @@ def get_body(self):
return json.dumps(self._get_body())

def get_headers(self):
return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X'}
return {'Accept': 'application/json', 'Content-Type': 'application/json', 'X-Requested-With': 'X', **self._headers}

@staticmethod
def _build_values(entity_type, entity):
Expand Down Expand Up @@ -512,13 +518,18 @@ class EntityModifyRequest(ODataHttpRequest):
Call execute() to send the update-request to the OData service
and get the modified entity."""

def __init__(self, url, connection, handler, entity_set, entity_key):
def __init__(self, url, connection, handler, entity_set, entity_key, method="PATCH"):
super(EntityModifyRequest, self).__init__(url, connection, handler)
self._logger = logging.getLogger(LOGGER_NAME)
self._entity_set = entity_set
self._entity_type = entity_set.entity_type
self._entity_key = entity_key

if method.upper() not in ["PATCH", "PUT"]:
raise ValueError("Method must be either PATCH or PUT")

self._method = method

self._values = {}

# get all properties declared by entity type
Expand All @@ -531,7 +542,7 @@ def get_path(self):

def get_method(self):
# pylint: disable=no-self-use
return 'PATCH'
return self._method

def get_body(self):
# pylint: disable=no-self-use
Expand All @@ -541,7 +552,7 @@ def get_body(self):
return json.dumps(body)

def get_headers(self):
return {'Accept': 'application/json', 'Content-Type': 'application/json'}
return {'Accept': 'application/json', 'Content-Type': 'application/json', **self._headers}

def set(self, **kwargs):
"""Set properties to be changed."""
Expand Down Expand Up @@ -639,6 +650,7 @@ def get_headers(self):

return {
'Accept': 'application/json',
**self._headers
}

def get_query_params(self):
Expand Down Expand Up @@ -699,6 +711,7 @@ def get_method(self):
def get_headers(self):
return {
'Accept': 'application/json',
**self._headers
}


Expand Down Expand Up @@ -956,6 +969,18 @@ def __eq__(self, value):
def __ne__(self, value):
return GetEntitySetFilter.format_filter(self._proprty, 'ne', value)

def __lt__(self, value):
return GetEntitySetFilter.format_filter(self._proprty, 'lt', value)

def __le__(self, value):
return GetEntitySetFilter.format_filter(self._proprty, 'le', value)

def __ge__(self, value):
return GetEntitySetFilter.format_filter(self._proprty, 'ge', value)

def __gt__(self, value):
return GetEntitySetFilter.format_filter(self._proprty, 'gt', value)


class GetEntitySetRequest(QueryRequest):
"""GET on EntitySet"""
Expand Down Expand Up @@ -1140,7 +1165,7 @@ def create_entity_handler(response):
return EntityCreateRequest(self._service.url, self._service.connection, create_entity_handler, self._entity_set,
self.last_segment)

def update_entity(self, key=None, **kwargs):
def update_entity(self, key=None, method="PATCH", **kwargs):
"""Updates an existing entity in the given entity-set."""

def update_entity_handler(response):
Expand All @@ -1158,7 +1183,7 @@ def update_entity_handler(response):
self._logger.info('Updating entity %s for key %s and args %s', self._entity_set.entity_type.name, key, kwargs)

return EntityModifyRequest(self._service.url, self._service.connection, update_entity_handler, self._entity_set,
entity_key)
entity_key, method=method)

def delete_entity(self, key: EntityKey = None, **kwargs):
"""Delete the entity"""
Expand Down Expand Up @@ -1433,7 +1458,7 @@ def get_boundary(self):

def get_headers(self):
# pylint: disable=no-self-use
return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary())}
return {'Content-Type': 'multipart/mixed;boundary={}'.format(self.get_boundary()), **self._headers}

def get_body(self):
return encode_multipart(self.get_boundary(), self.requests)
Expand Down
165 changes: 165 additions & 0 deletions tests/test_service_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""

Expand All @@ -671,6 +731,67 @@ def test_get_entity_with_entity_key_and_other_params(service):
query = service.entity_sets.TemperatureMeasurements.update_entity(key=key, Foo='Bar')
assert query.get_path() == "TemperatureMeasurements(Sensor='sensor1',Date=datetime'2017-12-24T18:00:00')"


def test_get_entities_with_custom_headers(service):
query = service.entity_sets.TemperatureMeasurements.get_entities()
query.add_headers({"X-Foo": "bar"})

assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"}


def test_get_entity_with_custom_headers(service):
key = EntityKey(
service.schema.entity_type('TemperatureMeasurement'),
Sensor='sensor1',
Date=datetime.datetime(2017, 12, 24, 18, 0))

query = service.entity_sets.TemperatureMeasurements.get_entity(key)
query.add_headers({"X-Foo": "bar"})

assert query.get_headers() == {"Accept": "application/json", "X-Foo": "bar"}


def test_update_entities_with_custom_headers(service):
key = EntityKey(
service.schema.entity_type('TemperatureMeasurement'),
Sensor='sensor1',
Date=datetime.datetime(2017, 12, 24, 18, 0))

query = service.entity_sets.TemperatureMeasurements.update_entity(key)
query.add_headers({"X-Foo": "bar"})

assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Foo": "bar"}


def test_create_entity_with_custom_headers(service):
query = service.entity_sets.TemperatureMeasurements.create_entity()
query.add_headers({"X-Foo": "bar"})

assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "X", "X-Foo": "bar"}


def test_create_entity_with_overwriting_custom_headers(service):
query = service.entity_sets.TemperatureMeasurements.create_entity()
query.add_headers({"X-Requested-With": "bar"})

assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "bar"}


def test_create_entity_with_blank_custom_headers(service):
query = service.entity_sets.TemperatureMeasurements.create_entity()
query.add_headers({})

assert query.get_headers() == {"Accept": "application/json", "Content-Type": "application/json", "X-Requested-With": "X"}


def test_pass_incorrect_header_type(service):
query = service.entity_sets.TemperatureMeasurements.create_entity()

with pytest.raises(TypeError) as ex:
query.add_headers(69420)
assert str(ex) == "TypeError: Headers must be of type 'dict' not <class 'int'>"


@responses.activate
def test_get_entities(service):
"""Get entities"""
Expand Down Expand Up @@ -1332,6 +1453,50 @@ def test_get_entity_set_query_filter_ne(service):
assert filter_str == "Key ne 'bar'"


def test_get_entity_set_query_filter_lt(service):
"""Test the operator 'lt' of $filter for humans"""

# pylint: disable=redefined-outer-name, invalid-name

request = service.entity_sets.Cars.get_entities()
filter_str = request.Price < 2

assert filter_str == "Price lt 2"


def test_get_entity_set_query_filter_le(service):
"""Test the operator 'le' of $filter for humans"""

# pylint: disable=redefined-outer-name, invalid-name

request = service.entity_sets.Cars.get_entities()
filter_str = request.Price <= 2

assert filter_str == "Price le 2"


def test_get_entity_set_query_filter_ge(service):
"""Test the operator 'ge' of $filter for humans"""

# pylint: disable=redefined-outer-name, invalid-name

request = service.entity_sets.Cars.get_entities()
filter_str = request.Price >= 2

assert filter_str == "Price ge 2"


def test_get_entity_set_query_filter_gt(service):
"""Test the operator 'gt' of $filter for humans"""

# pylint: disable=redefined-outer-name, invalid-name

request = service.entity_sets.Cars.get_entities()
filter_str = request.Price > 2

assert filter_str == "Price gt 2"


def test_get_entity_set_query_filter_and(service):
"""Test the operator 'and' of $filter for humans"""

Expand Down