Skip to content

Commit 5642bb7

Browse files
mamiksikfilak-sap
authored andcommitted
service: add etag property to EntityProxy
In OData V2 ETag is located in header or is part of the body in optional substructure called "__metadata". https://www.odata.org/documentation/odata-version-2-0/json-format/#7_representing_entries_8 ETag is included in both the header and body of response we check if they match. If not exception is raised which is our invention and does not need to be preserved if it causes any problems.
1 parent 1868980 commit 5642bb7

File tree

3 files changed

+58
-6
lines changed

3 files changed

+58
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1111
- Add a Service wide configuration (e.g. http.update\_method) - Jakub Filak
1212
- <, <=, >, >= operators on GetEntitySetFilter - Barton Ip
1313
- Django style filtering - Barton Ip
14+
- Add etag property to EntityProxy - Martin Miksik
1415

1516
### Fixed
1617
- URL encode $filter contents - Barton Ip

pyodata/v2/service.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -736,20 +736,28 @@ class EntityProxy:
736736

737737
# pylint: disable=too-many-branches,too-many-nested-blocks
738738

739-
def __init__(self, service, entity_set, entity_type, proprties=None, entity_key=None):
739+
def __init__(self, service, entity_set, entity_type, proprties=None, entity_key=None, etag=None):
740740
self._logger = logging.getLogger(LOGGER_NAME)
741741
self._service = service
742742
self._entity_set = entity_set
743743
self._entity_type = entity_type
744744
self._key_props = entity_type.key_proprties
745745
self._cache = dict()
746746
self._entity_key = entity_key
747+
self._etag = etag
747748

748749
self._logger.debug('New entity proxy instance of type %s from properties: %s', entity_type.name, proprties)
749750

750751
# cache values of individual properties if provided
751752
if proprties is not None:
752753

754+
etag_body = proprties.get('__metadata', dict()).get('etag', None)
755+
if etag is not None and etag_body is not None and etag_body != etag:
756+
raise PyODataException('Etag from header does not match the Etag from response body')
757+
758+
if etag_body is not None:
759+
self._etag = etag_body
760+
753761
# first, cache values of direct properties
754762
for type_proprty in self._entity_type.proprties():
755763
if type_proprty.name in proprties:
@@ -921,6 +929,11 @@ def url(self):
921929

922930
return urljoin(service_url, entity_path)
923931

932+
@property
933+
def etag(self):
934+
"""ETag generated by service"""
935+
return self._etag
936+
924937
def equals(self, other):
925938
"""Returns true if the self and the other contains the same data"""
926939
# pylint: disable=W0212
@@ -1336,8 +1349,9 @@ def get_entity_handler(response):
13361349
.format(self._name, response.status_code), response)
13371350

13381351
entity = response.json()['d']
1352+
etag = response.headers.get('ETag', None)
13391353

1340-
return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity)
1354+
return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity, etag=etag)
13411355

13421356
if key is not None and isinstance(key, EntityKey):
13431357
entity_key = key
@@ -1391,8 +1405,9 @@ def create_entity_handler(response):
13911405
.format(self._name, response.status_code), response)
13921406

13931407
entity_props = response.json()['d']
1408+
etag = response.headers.get('ETag', None)
13941409

1395-
return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity_props)
1410+
return EntityProxy(self._service, self._entity_set, self._entity_set.entity_type, entity_props, etag=etag)
13961411

13971412
return EntityCreateRequest(self._service.url, self._service.connection, create_entity_handler, self._entity_set,
13981413
self.last_segment)

tests/test_service_v2.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ def test_create_entity(service):
3333
responses.add(
3434
responses.POST,
3535
"{0}/MasterEntities".format(service.url),
36-
headers={'Content-type': 'application/json'},
36+
headers={
37+
'Content-type': 'application/json',
38+
'ETag': 'W/\"J0FtZXJpY2FuIEFpcmxpbmVzJw==\"'
39+
},
3740
json={'d': {
41+
'__metadata': {
42+
'etag': 'W/\"J0FtZXJpY2FuIEFpcmxpbmVzJw==\"',
43+
},
3844
'Key': '12345',
3945
'Data': 'abcd'
4046
}},
@@ -44,6 +50,7 @@ def test_create_entity(service):
4450

4551
assert result.Key == '12345'
4652
assert result.Data == 'abcd'
53+
assert result.etag == 'W/\"J0FtZXJpY2FuIEFpcmxpbmVzJw==\"'
4754

4855

4956
@responses.activate
@@ -197,11 +204,16 @@ def test_get_entity_property(service):
197204
responses.add(
198205
responses.GET,
199206
"{0}/MasterEntities('12345')".format(service.url),
200-
headers={'Content-type': 'application/json'},
207+
headers={
208+
'ETag': 'W/\"J0FtZXJpY2FuIEFpcmxpbmVzJw==\"',
209+
'Content-type': 'application/json',
210+
},
201211
json={'d': {'Key': '12345'}},
202212
status=200)
203213

204-
assert service.entity_sets.MasterEntities.get_entity('12345').execute().Key == '12345'
214+
result = service.entity_sets.MasterEntities.get_entity('12345').execute()
215+
assert result.Key == '12345'
216+
assert result.etag == 'W/\"J0FtZXJpY2FuIEFpcmxpbmVzJw==\"'
205217

206218

207219
@responses.activate
@@ -2129,6 +2141,30 @@ def test_parsing_of_datetime_before_unix_time(service):
21292141
assert result.Date == datetime.datetime(1945, 5, 8, 19, 0, tzinfo=datetime.timezone.utc)
21302142

21312143

2144+
@responses.activate
2145+
def test_mismatched_etags_in_body_and_header(service):
2146+
"""Test creating entity with missmatched etags"""
2147+
2148+
responses.add(
2149+
responses.POST,
2150+
"{0}/MasterEntities".format(service.url),
2151+
headers={
2152+
'Content-type': 'application/json',
2153+
'ETag': 'W/\"JEF\"'
2154+
},
2155+
json={'d': {
2156+
'__metadata': {
2157+
'etag': 'W/\"PEF\"',
2158+
}
2159+
}},
2160+
status=201)
2161+
2162+
with pytest.raises(PyODataException) as e_info:
2163+
service.entity_sets.MasterEntities.create_entity().set(**{}).execute()
2164+
2165+
assert str(e_info.value) == 'Etag from header does not match the Etag from response body'
2166+
2167+
21322168
def test_odata_http_response():
21332169
"""Test that ODataHttpResponse is complaint with requests.Reponse"""
21342170

0 commit comments

Comments
 (0)