Skip to content

Commit adf376f

Browse files
mamiksikfilak-sap
authored andcommitted
Fix datetime handling while creating/updating entity
For most of EDM types, their value and json representation are the same. One of the types with different behaviour is DateTime which is in json in OData2 represented as "/Data(xxxx)/" instead of simply timestamp xxxx. Thus, the need for 'to_json' function which for most data types solely returns value. However, for DateTime, it returns a string in a format mentioned above. Reference links: For literal format see "6. Primitive Data Types" at https://www.odata.org/documentation/odata-version-2-0/overview/ For json format definition see "4. Primitive Types" at https://www.odata.org/documentation/odata-version-2-0/json-format/
1 parent 66123b0 commit adf376f

File tree

3 files changed

+106
-5
lines changed

3 files changed

+106
-5
lines changed

pyodata/v2/model.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,9 @@ def to_literal(self, value):
322322
def from_json(self, value):
323323
return value
324324

325+
def to_json(self, value):
326+
return value
327+
325328
def from_literal(self, value):
326329
return value
327330

@@ -376,6 +379,14 @@ def to_literal(self, value):
376379

377380
return super(EdmDateTimeTypTraits, self).to_literal(value.isoformat())
378381

382+
def to_json(self, value):
383+
if isinstance(value, str):
384+
return value
385+
386+
# Converts datetime into timestamp in milliseconds in UTC timezone as defined in ODATA specification
387+
# https://www.odata.org/documentation/odata-version-2-0/json-format/
388+
return f'/Date({int(value.replace(tzinfo=datetime.timezone.utc).timestamp()) * 1000})/'
389+
379390
def from_json(self, value):
380391

381392
if value is None:
@@ -389,7 +400,7 @@ def from_json(self, value):
389400

390401
try:
391402
# https://stackoverflow.com/questions/36179914/timestamp-out-of-range-for-platform-localtime-gmtime-function
392-
value = datetime.datetime(1970, 1, 1) + datetime.timedelta(milliseconds=int(value))
403+
value = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc) + datetime.timedelta(milliseconds=int(value))
393404
except ValueError:
394405
raise PyODataModelError('Cannot decode datetime from value {}.'.format(value))
395406

pyodata/v2/service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -455,7 +455,7 @@ def _build_values(entity_type, entity):
455455
values = {}
456456
for key, val in entity.items():
457457
try:
458-
entity_type.proprty(key)
458+
val = entity_type.proprty(key).typ.traits.to_json(val)
459459
except KeyError:
460460
try:
461461
nav_prop = entity_type.nav_proprty(key)
@@ -542,7 +542,7 @@ def set(self, **kwargs):
542542

543543
for key, val in kwargs.items():
544544
try:
545-
self._entity_type.proprty(key)
545+
val = self._entity_type.proprty(key).typ.traits.to_json(val)
546546
except KeyError:
547547
raise PyODataException(
548548
'Property {} is not declared in {} entity type'.format(key, self._entity_type.name))

tests/test_service_v2.py

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import requests
66
import pytest
77
from unittest.mock import patch
8+
89
import pyodata.v2.model
910
import pyodata.v2.service
1011
from pyodata.exceptions import PyODataException, HttpError, ExpressionError
@@ -277,7 +278,7 @@ def test_entity_key_complex(service):
277278
entity = service.entity_sets.TemperatureMeasurements.get_entity(key=None, **entity_key).execute()
278279
assert key_properties == set(entity_property.name for entity_property in entity.entity_key.key_properties)
279280
# check also python represantation of date
280-
assert entity.Date == datetime.datetime(2017, 12, 24, 18, 0)
281+
assert entity.Date == datetime.datetime(2017, 12, 24, 18, 0, tzinfo=datetime.timezone.utc)
281282

282283

283284
def test_get_entity_property_complex_key(service):
@@ -574,7 +575,7 @@ def test_update_entity(service):
574575
"{0}/TemperatureMeasurements(Sensor='sensor1',Date=datetime'2017-12-24T18:00:00')".format(service.url),
575576
json={'d': {
576577
'Sensor': 'Sensor-address',
577-
'Date': "datetime'2017-12-24T18:00'",
578+
'Date': "/Date(1714138400000)/",
578579
'Value': 34
579580
}},
580581
status=204)
@@ -586,6 +587,16 @@ def test_update_entity(service):
586587
assert isinstance(request, pyodata.v2.service.EntityModifyRequest)
587588

588589
request.set(Value=34)
590+
# Tests if update entity correctly calls 'to_json' method
591+
request.set(Date=datetime.datetime(2017, 12, 24, 19, 0))
592+
593+
assert request._values['Value'] == 34
594+
assert request._values['Date'] == '/Date(1514142000000)/'
595+
596+
# If preformatted datetime is passed (e. g. you already replaced datetime instance with string which is
597+
# complaint with odata specification), 'to_json' does not update given value (for backward compatibility reasons)
598+
request.set(Date='/Date(1714138400000)/')
599+
assert request._values['Date'] == '/Date(1714138400000)/'
589600

590601
request.execute()
591602

@@ -1418,3 +1429,82 @@ def test_navigation_count_with_filter(service):
14181429
assert isinstance(request, pyodata.v2.service.GetEntitySetRequest)
14191430

14201431
assert request.execute() == 3
1432+
1433+
1434+
@responses.activate
1435+
def test_create_entity_with_datetime(service):
1436+
"""
1437+
Basic test on creating entity with datetime
1438+
Also tzinfo is set to simulate user passing datetime object with different timezone than UTC
1439+
"""
1440+
1441+
# https://stackoverflow.com/questions/17976063/how-to-create-tzinfo-when-i-have-utc-offset
1442+
class MyUTCOffsetTimezone(datetime.tzinfo):
1443+
1444+
def __init__(self, offset=19800, name=None):
1445+
self.offset = datetime.timedelta(seconds=offset)
1446+
self.name = name or self.__class__.__name__
1447+
1448+
def utcoffset(self, dt):
1449+
return self.offset
1450+
1451+
def tzname(self, dt):
1452+
return self.name
1453+
1454+
def dst(self, dt):
1455+
return datetime.timedelta(0)
1456+
1457+
# pylint: disable=redefined-outer-name
1458+
1459+
responses.add(
1460+
responses.POST,
1461+
"{0}/TemperatureMeasurements".format(service.url),
1462+
headers={'Content-type': 'application/json'},
1463+
json={'d': {
1464+
'Sensor': 'Sensor1',
1465+
'Date': '/Date(1514138400000)/',
1466+
'Value': '34'
1467+
}},
1468+
status=201)
1469+
1470+
1471+
# Offset -18000 sec is for America/Chicago (CDT) timezone
1472+
request = service.entity_sets.TemperatureMeasurements.create_entity().set(**{
1473+
'Sensor': 'Sensor1',
1474+
'Date': datetime.datetime(2017, 12, 24, 18, 0, tzinfo=MyUTCOffsetTimezone(-18000)),
1475+
'Value': 34
1476+
})
1477+
1478+
assert request._values['Date'] == '/Date(1514138400000)/'
1479+
1480+
result = request.execute()
1481+
assert result.Date == datetime.datetime(2017, 12, 24, 18, 0, tzinfo=datetime.timezone.utc)
1482+
1483+
1484+
@responses.activate
1485+
def test_parsing_of_datetime_before_unix_time(service):
1486+
"""Test DateTime handling of time before 1970"""
1487+
1488+
# pylint: disable=redefined-outer-name
1489+
1490+
responses.add(
1491+
responses.POST,
1492+
"{0}/TemperatureMeasurements".format(service.url),
1493+
headers={'Content-type': 'application/json'},
1494+
json={'d': {
1495+
'Sensor': 'Sensor1',
1496+
'Date': '/Date(-777877200000)/',
1497+
'Value': '34'
1498+
}},
1499+
status=201)
1500+
1501+
request = service.entity_sets.TemperatureMeasurements.create_entity().set(**{
1502+
'Sensor': 'Sensor1',
1503+
'Date': datetime.datetime(1945, 5, 8, 19, 0),
1504+
'Value': 34
1505+
})
1506+
1507+
assert request._values['Date'] == '/Date(-777877200000)/'
1508+
1509+
result = request.execute()
1510+
assert result.Date == datetime.datetime(1945, 5, 8, 19, 0, tzinfo=datetime.timezone.utc)

0 commit comments

Comments
 (0)