From 71f8973bb7943494952ee7bd359f825c2b2099ee Mon Sep 17 00:00:00 2001 From: annatisch Date: Mon, 9 May 2016 17:24:40 -0700 Subject: [PATCH] support for unix timestamps --- .../AcceptanceTests/integer_tests.py | 10 +- .../Python/Python/ClientModelExtensions.cs | 8 +- .../Python/Python/PythonCodeNamer.cs | 2 +- .../Python/msrest/msrest/serialization.py | 114 +++++++++++++++--- 4 files changed, 114 insertions(+), 20 deletions(-) diff --git a/AutoRest/Generators/Python/Python.Tests/AcceptanceTests/integer_tests.py b/AutoRest/Generators/Python/Python.Tests/AcceptanceTests/integer_tests.py index 9b41bc6817..d1b000739c 100644 --- a/AutoRest/Generators/Python/Python.Tests/AcceptanceTests/integer_tests.py +++ b/AutoRest/Generators/Python/Python.Tests/AcceptanceTests/integer_tests.py @@ -30,7 +30,7 @@ import isodate import tempfile import json -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, tzinfo import os from os.path import dirname, pardir, join, realpath, sep, pardir @@ -69,6 +69,14 @@ def test_integer(self): #client.int_model.get_underflow_int32() #client.int_model.get_underflow_int64() + unix_date = datetime(year=2016, month=4, day=13) + client.int_model.put_unix_time_date(unix_date) + self.assertEqual(unix_date.utctimetuple(), client.int_model.get_unix_time().utctimetuple()) + self.assertIsNone(client.int_model.get_null_unix_time()) + + with self.assertRaises(DeserializationError): + client.int_model.get_invalid_unix_time() + if __name__ == '__main__': unittest.main() \ No newline at end of file diff --git a/AutoRest/Generators/Python/Python/ClientModelExtensions.cs b/AutoRest/Generators/Python/Python/ClientModelExtensions.cs index d43e67525c..330d8aef56 100644 --- a/AutoRest/Generators/Python/Python/ClientModelExtensions.cs +++ b/AutoRest/Generators/Python/Python/ClientModelExtensions.cs @@ -147,6 +147,11 @@ public static string ToPythonRuntimeTypeString(this IType type) { return "duration"; } + + if (known.Type == KnownPrimaryType.UnixTime) + { + return "unix-time"; + } } var sequenceType = type as SequenceType; @@ -246,7 +251,8 @@ public static string GetPythonSerializationType(IType type) { { KnownPrimaryType.DateTime, "iso-8601" }, { KnownPrimaryType.DateTimeRfc1123, "rfc-1123" }, - { KnownPrimaryType.TimeSpan, "duration" } + { KnownPrimaryType.TimeSpan, "duration" }, + { KnownPrimaryType.UnixTime, "unix-time" } }; PrimaryType primaryType = type as PrimaryType; if (primaryType != null) diff --git a/AutoRest/Generators/Python/Python/PythonCodeNamer.cs b/AutoRest/Generators/Python/Python/PythonCodeNamer.cs index 11ae0801ac..bcc2eceecf 100644 --- a/AutoRest/Generators/Python/Python/PythonCodeNamer.cs +++ b/AutoRest/Generators/Python/Python/PythonCodeNamer.cs @@ -378,7 +378,7 @@ private static IType NormalizePrimaryType(PrimaryType primaryType) } else if (primaryType.Type == KnownPrimaryType.UnixTime) { - primaryType.Name = "long"; + primaryType.Name = "datetime"; } else if (primaryType.Type == KnownPrimaryType.Object) // Revisit here { diff --git a/ClientRuntimes/Python/msrest/msrest/serialization.py b/ClientRuntimes/Python/msrest/msrest/serialization.py index 4675cd6aac..f1bd12f91c 100644 --- a/ClientRuntimes/Python/msrest/msrest/serialization.py +++ b/ClientRuntimes/Python/msrest/msrest/serialization.py @@ -25,10 +25,13 @@ # -------------------------------------------------------------------------- from base64 import b64decode, b64encode +import calendar import datetime import decimal from enum import Enum +import importlib import json +import logging import re try: from urllib import quote @@ -49,6 +52,31 @@ except NameError: basestring = str +_LOGGER = logging.getLogger(__name__) + + +class UTC(datetime.tzinfo): + """Time Zone info for handling UTC""" + + def utcoffset(self, dt): + """UTF offset for UTC is 0.""" + return datetime.timedelta(0) + + def tzname(self, dt): + """Timestamp representation.""" + return "Z" + + def dst(self, dt): + """No daylight saving for UTC.""" + return datetime.timedelta(hours=1) + + +try: + from datetime import timezone + TZ_UTC = timezone.utc +except ImportError: + TZ_UTC = UTC() + class Model(object): """Mixin for all client request body/response body models to support @@ -112,6 +140,24 @@ def _classify(cls, response, objects): raise TypeError("Object cannot be classified futher.") +def _convert_to_datatype(params, localtype): + """Convert a dict-like object to the given datatype + """ + return _recursive_convert_to_datatype(params, localtype.__name__, importlib.import_module('..', localtype.__module__)) + + +def _recursive_convert_to_datatype(params, str_localtype, models_module): + """Convert a dict-like object to the given datatype + """ + if isinstance(params, list): + return [_recursive_convert_to_datatype(data, str_localtype[1:-1], models_module) for data in params] + localtype = getattr(models_module, str_localtype) if hasattr(models_module, str_localtype) else None + if not localtype: + return params + result = {key: _recursive_convert_to_datatype(params[key], localtype._attribute_map[key]['type'], models_module) for key in params} + return localtype(**result) + + class Serializer(object): """Request object model serializer.""" @@ -139,6 +185,7 @@ def __init__(self): self.serialize_type = { 'iso-8601': Serializer.serialize_iso, 'rfc-1123': Serializer.serialize_rfc, + 'unix-time': Serializer.serialize_unix, 'duration': Serializer.serialize_duration, 'date': Serializer.serialize_date, 'decimal': Serializer.serialize_decimal, @@ -235,7 +282,11 @@ def body(self, data, data_type, **kwargs): """ if data is None: raise ValidationError("required", "body", True) - return self._serialize(data, data_type, **kwargs) + elif isinstance(data_type, str): + return self._serialize(data, data_type, **kwargs) + elif not isinstance(data, data_type): + data = _convert_to_datatype(data, data_type) + return self._serialize(data, data_type.__name__, **kwargs) def url(self, name, data, data_type, **kwargs): """Serialize data intended for a URL path. @@ -527,6 +578,8 @@ def serialize_rfc(attr, **kwargs): :raises: TypeError if format invalid. """ try: + if not attr.tzinfo: + _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") utc = attr.utctimetuple() except AttributeError: raise TypeError("RFC1123 object must be valid Datetime object.") @@ -546,9 +599,14 @@ def serialize_iso(attr, **kwargs): """ if isinstance(attr, str): attr = isodate.parse_datetime(attr) - try: - utc = attr.utctimetuple() + try: + if not attr.tzinfo: + _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") + utc = attr.utctimetuple() + except AttributeError: + raise TypeError( + "ISO-8601 object must be valid Datetime object.") if utc.tm_year > 9999 or utc.tm_year < 1: raise OverflowError("Hit max or min date") @@ -561,6 +619,24 @@ def serialize_iso(attr, **kwargs): msg = "Unable to serialize datetime object." raise_with_traceback(SerializationError, msg, err) + @staticmethod + def serialize_unix(attr, **kwargs): + """Serialize Datetime object into IntTime format. + This is represented as seconds. + + :param Datetime attr: Object to be serialized. + :rtype: int + :raises: SerializationError if format invalid + """ + if isinstance(attr, int): + return attr + try: + if not attr.tzinfo: + _LOGGER.warning("Datetime with no tzinfo will be considered UTC.") + return int(calendar.timegm(attr.utctimetuple())) + except AttributeError: + raise TypeError("Unix time object must be valid Datetime object.") + class Deserializer(object): """Response object model deserializer. @@ -579,6 +655,7 @@ def __init__(self, classes={}): self.deserialize_type = { 'iso-8601': Deserializer.deserialize_iso, 'rfc-1123': Deserializer.deserialize_rfc, + 'unix-time': Deserializer.deserialize_unix, 'duration': Deserializer.deserialize_duration, 'date': Deserializer.deserialize_date, 'decimal': Deserializer.deserialize_decimal, @@ -958,7 +1035,8 @@ def deserialize_rfc(attr): try: date_obj = datetime.datetime.strptime( attr, "%a, %d %b %Y %H:%M:%S %Z") - date_obj = date_obj.replace(tzinfo=UTC()) + if not date_obj.tzinfo: + date_obj = date_obj.replace(tzinfo=TZ_UTC) except ValueError as err: msg = "Cannot deserialize to rfc datetime object." raise_with_traceback(DeserializationError, msg, err) @@ -1000,18 +1078,20 @@ def deserialize_iso(attr): else: return date_obj + @staticmethod + def deserialize_unix(attr): + """Serialize Datetime object into IntTime format. + This is represented as seconds. -class UTC(datetime.tzinfo): - """Time Zone info for handling UTC""" - - def utcoffset(self, dt): - """UTF offset for UTC is 0.""" - return datetime.timedelta(hours=0, minutes=0) - - def tzname(self, dt): - """Timestamp representation.""" - return "Z" + :param int attr: Object to be serialized. + :rtype: Datetime + :raises: DeserializationError if format invalid + """ + try: + date_obj = datetime.datetime.fromtimestamp(attr, TZ_UTC) + except ValueError as err: + msg = "Cannot deserialize to unix datetime object." + raise_with_traceback(DeserializationError, msg, err) + else: + return date_obj - def dst(self, dt): - """No daylight saving for UTC.""" - return datetime.timedelta(0)