Repository: SAP/python-pyodata

Issue:
Date out of range error
According to #80 we facing the same error. We're just consumer using the api to receive data for our DWH. Currently one of our job fails because of this error.

Is it possible to implement a fix for this problem on pyodata side?

Json:
```
{
    "d": {
        "results": {
            "__metadata": {
                "uri": "https://service.url/sap/c4c/odata/v1/c4codataapi/CorporateAccountIdentificationCollection('12345678910111213ABCDEFGHIJKLMNO')",
                "type": "c4codata.CorporateAccountIdentification",
                "etag": "W/"datetimeoffset'2021-02-01T19%3A41%3A17.9430080Z'""
            },
            "ObjectID": "12345678910111213ABCDEFGHIJKLMNO",
            "ParentObjectID": "166345678910111213ABCDEFGHIJKLMNO",
            "ETag": "/Date(1612208477943)/",
            "AccountID": "12345",
            "IDTypeCode": "ABCDE",
            "IDTypeCodeText": "<ABCD12>",
            "IDNumber": "1234-123",
            "ResponsibleInstitution": "",
            "EntryDate": null,
            "ValidFrom": "/Date(-62135769600000)/",
            "ValidTo": "/Date(253402214400000)/",
            "CountryCode": "",
            "CountryCodeText": "",
            "StateCode": "",
            "StateCodeText": "",
            "ZZWECK_KUT": "",
            "ZZWECK_KUTText": "",
            "CorporateAccount": {
                "__deferred": {
                    "uri": "https://service.url/sap/c4c/odata/v1/c4codataapi/CorporateAccountIdentificationCollection('12345678910111213ABCDEFGHIJKLMNO')/CorporateAccount"
                }
            }
        }
    }
}
```

Thanks & BR


Comments:

Patch:
diff --git a/pyodata/v2/model.py b/pyodata/v2/model.py
index 5412ca9..67cc1ad 100644
--- a/pyodata/v2/model.py
+++ b/pyodata/v2/model.py
@@ -21,6 +21,8 @@ from lxml import etree
 from pyodata.exceptions import PyODataException, PyODataModelError, PyODataParserError
 
 LOGGER_NAME = 'pyodata.model'
+FIX_SCREWED_UP_MINIMAL_DATETIME_VALUE = False
+FIX_SCREWED_UP_MAXIMUM_DATETIME_VALUE = False
 
 IdentifierInfo = collections.namedtuple('IdentifierInfo', 'namespace name')
 TypeInfo = collections.namedtuple('TypeInfo', 'namespace name is_collection')
@@ -414,8 +416,19 @@ class EdmDateTimeTypTraits(EdmPrefixedTypTraits):
         try:
             # https://stackoverflow.com/questions/36179914/timestamp-out-of-range-for-platform-localtime-gmtime-function
             value = datetime.datetime(1970, 1, 1, tzinfo=current_timezone()) + datetime.timedelta(milliseconds=int(value))
-        except ValueError:
-            raise PyODataModelError(f'Cannot decode datetime from value {value}.')
+        except (ValueError, OverflowError):
+            if FIX_SCREWED_UP_MINIMAL_DATETIME_VALUE and int(value) < -62135596800000:
+                # Some service providers return false minimal date values.
+                # -62135596800000 is the lowest value PyOData could read.
+                # This workaroud fixes this issue and returns 0001-01-01 00:00:00+00:00 in such a case.
+                value = datetime.datetime(year=1, day=1, month=1, tzinfo=current_timezone())
+            elif FIX_SCREWED_UP_MAXIMUM_DATETIME_VALUE and int(value) > 253402300799999:
+                value = datetime.datetime(year=9999, day=31, month=12, tzinfo=current_timezone())
+            else:
+                raise PyODataModelError(f'Cannot decode datetime from value {value}. '
+                                        f'Possible value range: -62135596800000 to 253402300799999. '
+                                        f'You may fix this by setting `FIX_SCREWED_UP_MINIMAL_DATETIME_VALUE` '
+                                        f' or `FIX_SCREWED_UP_MAXIMUM_DATETIME_VALUE` as a workaround.')
 
         return value
 


Test patch:
diff --git a/tests/test_model_v2.py b/tests/test_model_v2.py
index ca72430..8f7ecef 100644
--- a/tests/test_model_v2.py
+++ b/tests/test_model_v2.py
@@ -9,6 +9,7 @@ from pyodata.v2.model import Schema, Typ, StructTypeProperty, Types, EntityType,
     PolicyIgnore, Config, PolicyFatal, NullType, NullAssociation, current_timezone, StructType
 from pyodata.exceptions import PyODataException, PyODataModelError, PyODataParserError
 from tests.conftest import assert_logging_policy
+import pyodata.v2.model
 
 
 def test_edmx(schema):
@@ -537,10 +538,20 @@ def test_traits_datetime():
     assert testdate.microsecond == 0
     assert testdate.tzinfo == current_timezone()
 
+    # parsing below lowest value with workaround
+    pyodata.v2.model.FIX_SCREWED_UP_MINIMAL_DATETIME_VALUE = True
+    testdate = typ.traits.from_json("/Date(-62135596800001)/")
+    assert testdate.year == 1
+    assert testdate.month == 1
+    assert testdate.day == 1
+    assert testdate.tzinfo == current_timezone()
+
     # parsing the lowest value
-    with pytest.raises(OverflowError):
+    pyodata.v2.model.FIX_SCREWED_UP_MINIMAL_DATETIME_VALUE = False
+    with pytest.raises(PyODataModelError) as e_info:
         typ.traits.from_json("/Date(-62135596800001)/")
-
+    assert str(e_info.value).startswith('Cannot decode datetime from value -62135596800001.')
+       
     testdate = typ.traits.from_json("/Date(-62135596800000)/")
     assert testdate.year == 1
     assert testdate.month == 1
@@ -551,9 +562,19 @@ def test_traits_datetime():
     assert testdate.microsecond == 0
     assert testdate.tzinfo == current_timezone()
 
+    # parsing above highest value with workaround
+    pyodata.v2.model.FIX_SCREWED_UP_MAXIMUM_DATETIME_VALUE = True
+    testdate = typ.traits.from_json("/Date(253402300800000)/")
+    assert testdate.year == 9999
+    assert testdate.month == 12
+    assert testdate.day == 31
+    assert testdate.tzinfo == current_timezone()
+
     # parsing the highest value
-    with pytest.raises(OverflowError):
+    pyodata.v2.model.FIX_SCREWED_UP_MAXIMUM_DATETIME_VALUE = False
+    with pytest.raises(PyODataModelError) as e_info:
         typ.traits.from_json("/Date(253402300800000)/")
+    assert str(e_info.value).startswith('Cannot decode datetime from value 253402300800000.')
 
     testdate = typ.traits.from_json("/Date(253402300799999)/")
     assert testdate.year == 9999