Skip to content

Commit

Permalink
model: set UTC timezone to DateTimes read rom JSON too
Browse files Browse the repository at this point in the history
I came a cross this difference during testing of #95

I decided to set tzinfo of JSON DateTimes to simplify my tests and it
also make sense to me to have the values configured the same way.
  • Loading branch information
filak-sap committed Apr 30, 2020
1 parent f9144b3 commit 16a15fb
Show file tree
Hide file tree
Showing 3 changed files with 25 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

### Changed
- handle GET EntitySet payload without the member results - Jakub Filak
- both Literal and JSON DateTimes has Timezone set to UTC - Jakub Filak

### Fixed
- removed superfluous debug print when parsing FunctionImports from metadata - Jakub Filak
Expand Down
17 changes: 14 additions & 3 deletions pyodata/v2/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@
TypeInfo = collections.namedtuple('TypeInfo', 'namespace name is_collection')


def current_timezone():
"""Default Timezone for Python datetime instances when parsed from
Edm.DateTime values and vice versa.
OData V2 does not mention Timezones in the documentation of
Edm.DateTime and UTC was chosen because it is universal.
"""

return datetime.timezone.utc


def modlog():
return logging.getLogger(LOGGER_NAME)

Expand Down Expand Up @@ -387,7 +398,7 @@ def to_json(self, value):

# Converts datetime into timestamp in milliseconds in UTC timezone as defined in ODATA specification
# https://www.odata.org/documentation/odata-version-2-0/json-format/
return f'/Date({int(value.replace(tzinfo=datetime.timezone.utc).timestamp()) * 1000})/'
return f'/Date({int(value.replace(tzinfo=current_timezone()).timestamp()) * 1000})/'

def from_json(self, value):

Expand All @@ -402,7 +413,7 @@ def from_json(self, value):

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

Expand All @@ -426,7 +437,7 @@ def from_literal(self, value):
except ValueError:
raise PyODataModelError('Cannot decode datetime from value {}.'.format(value))

return value
return value.replace(tzinfo=current_timezone())


class EdmStringTypTraits(TypTraits):
Expand Down
12 changes: 10 additions & 2 deletions tests/test_model_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
from pyodata.v2.model import Schema, Typ, StructTypeProperty, Types, EntityType, EdmStructTypeSerializer, \
Association, AssociationSet, EndRole, AssociationSetEndRole, TypeInfo, MetadataBuilder, ParserError, PolicyWarning, \
PolicyIgnore, Config, PolicyFatal, NullType, NullAssociation
PolicyIgnore, Config, PolicyFatal, NullType, NullAssociation, current_timezone
from pyodata.exceptions import PyODataException, PyODataModelError, PyODataParserError
from tests.conftest import assert_logging_policy

Expand Down Expand Up @@ -458,7 +458,7 @@ def test_traits_datetime():

# 1. direction Python -> OData

testdate = datetime(2005, 1, 28, 18, 30, 44, 123456, tzinfo=timezone.utc)
testdate = datetime(2005, 1, 28, 18, 30, 44, 123456, tzinfo=current_timezone())
assert typ.traits.to_literal(testdate) == "datetime'2005-01-28T18:30:44.123456'"

# without miliseconds part
Expand All @@ -481,19 +481,22 @@ def test_traits_datetime():
assert testdate.minute == 33
assert testdate.second == 6
assert testdate.microsecond == 654321
assert testdate.tzinfo == current_timezone()

# parsing without miliseconds
testdate = typ.traits.from_literal("datetime'1976-11-23T03:33:06'")
assert testdate.year == 1976
assert testdate.second == 6
assert testdate.microsecond == 0
assert testdate.tzinfo == current_timezone()

# parsing without seconds and miliseconds
testdate = typ.traits.from_literal("datetime'1976-11-23T03:33'")
assert testdate.year == 1976
assert testdate.minute == 33
assert testdate.second == 0
assert testdate.microsecond == 0
assert testdate.tzinfo == current_timezone()

# parsing invalid value
with pytest.raises(PyODataModelError) as e_info:
Expand All @@ -515,19 +518,22 @@ def test_traits_datetime():
assert testdate.minute == 33
assert testdate.second == 6
assert testdate.microsecond == 10000
assert testdate.tzinfo == current_timezone()

# parsing without miliseconds
testdate = typ.traits.from_json("/Date(217567986000)/")
assert testdate.year == 1976
assert testdate.second == 6
assert testdate.microsecond == 0
assert testdate.tzinfo == current_timezone()

# parsing without seconds and miliseconds
testdate = typ.traits.from_json("/Date(217567980000)/")
assert testdate.year == 1976
assert testdate.minute == 33
assert testdate.second == 0
assert testdate.microsecond == 0
assert testdate.tzinfo == current_timezone()

# parsing the lowest value
with pytest.raises(OverflowError):
Expand All @@ -541,6 +547,7 @@ def test_traits_datetime():
assert testdate.minute == 0
assert testdate.second == 0
assert testdate.microsecond == 0
assert testdate.tzinfo == current_timezone()

# parsing the highest value
with pytest.raises(OverflowError):
Expand All @@ -554,6 +561,7 @@ def test_traits_datetime():
assert testdate.minute == 59
assert testdate.second == 59
assert testdate.microsecond == 999000
assert testdate.tzinfo == current_timezone()

# parsing invalid value
with pytest.raises(PyODataModelError) as e_info:
Expand Down

0 comments on commit 16a15fb

Please sign in to comment.