diff --git a/openstack/format.py b/openstack/format.py index 47902ac6..c50771b3 100644 --- a/openstack/format.py +++ b/openstack/format.py @@ -10,31 +10,69 @@ # License for the specific language governing permissions and limitations # under the License. +from datetime import datetime +import six -class BoolStr(object): - """A custom boolean/string hybrid type for resource.props. +from oslo_utils import timeutils - Translates a given value to the desired type. - """ - def __init__(self, given): - """A boolean parser. - Interprets the given value as a boolean, ignoring whitespace and case. - A TypeError is raised when interpreted as neither True nor False. - """ - expr = str(given).lower() - if 'true' == expr: - self.parsed = True - elif 'false' == expr: - self.parsed = False +class Formatter(object): + + @classmethod + def serialize(cls, value): + """Return a string representing the formatted value""" + raise NotImplementedError + + @classmethod + def deserialize(cls, value): + """Return a formatted object representing the value""" + raise NotImplementedError + + +class ISO8601(Formatter): + + @classmethod + def serialize(cls, value): + """Convert a datetime to an ISO8601 string""" + if isinstance(value, datetime): + return value.isoformat() + elif isinstance(value, six.string_types): + # If we're already given a string, keep it as-is. + # This happens when a string comes back in a response body, + # as opposed to the datetime case above which happens when + # a user is setting a datetime for a request. + return value else: - msg = 'Invalid as boolean: %s' % given - raise ValueError(msg) + raise ValueError("Unable to serialize ISO8601: %s" % value) - def __bool__(self): - return self.parsed + @classmethod + def deserialize(cls, value): + """Convert an ISO8601 string to a datetime object""" + if isinstance(value, six.string_types): + return timeutils.parse_isotime(value) + else: + raise ValueError("Unable to deserialize ISO8601: %s" % value) - __nonzero__ = __bool__ - def __str__(self): - return str(self.parsed) +class BoolStr(Formatter): + + # The behavior here primarily exists for the deserialize method + # to be producing Python booleans. + + @classmethod + def deserialize(cls, value): + return cls.convert(value) + + @classmethod + def serialize(cls, value): + return cls.convert(value) + + @classmethod + def convert(cls, value): + expr = str(value).lower() + if "true" == expr: + return True + elif "false" == expr: + return False + else: + raise ValueError("Unable to convert as boolean: %s" % value) diff --git a/openstack/metric/v1/resource.py b/openstack/metric/v1/resource.py index f776abc5..26ba69a2 100644 --- a/openstack/metric/v1/resource.py +++ b/openstack/metric/v1/resource.py @@ -9,9 +9,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from openstack import format from openstack.metric import metric_service from openstack import resource -from openstack import resource_types class Generic(resource.Resource): @@ -38,10 +38,10 @@ class Generic(resource.Resource): project_id = resource.prop('project_id') #: Timestamp when this resource was started started_at = resource.prop('started_at', - type=resource_types.ISO8601Datetime) + type=format.ISO8601) #: Timestamp when this resource was ended ended_at = resource.prop('ended_at', - type=resource_types.ISO8601Datetime) + type=format.ISO8601) #: A dictionary of metrics collected on this resource metrics = resource.prop('metrics', type=dict) diff --git a/openstack/resource.py b/openstack/resource.py index f57fe0be..b3daaaed 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -39,6 +39,7 @@ import six from six.moves.urllib import parse as url_parse from openstack import exceptions +from openstack import format from openstack import utils @@ -120,11 +121,10 @@ class prop(object): value = self.type({self.type.id_attribute: value}) else: value = self.type(value) + elif issubclass(self.type, format.Formatter): + value = self.type.deserialize(value) else: value = self.type(value) - attr = getattr(value, 'parsed', None) - if attr is not None: - value = attr return value @@ -136,6 +136,8 @@ class prop(object): value = self.type({self.type.id_attribute: value}) else: value = self.type(value) + elif issubclass(self.type, format.Formatter): + value = self.type.serialize(value) else: value = str(self.type(value)) # validate to fail fast diff --git a/openstack/resource_types.py b/openstack/resource_types.py deleted file mode 100644 index ef99d955..00000000 --- a/openstack/resource_types.py +++ /dev/null @@ -1,20 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import datetime - -from oslo_utils import timeutils - - -class ISO8601Datetime(datetime.datetime): - def __new__(cls, s): - if s: - return timeutils.parse_isotime(s) diff --git a/openstack/tests/unit/test_format.py b/openstack/tests/unit/test_format.py index f9bf7051..1c50b820 100644 --- a/openstack/tests/unit/test_format.py +++ b/openstack/tests/unit/test_format.py @@ -9,33 +9,68 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + +from datetime import datetime +from iso8601 import iso8601 import testtools from openstack import format -class TestFormat(testtools.TestCase): - def test_parse_true(self): - self.assertTrue(format.BoolStr(True).parsed) - self.assertTrue(format.BoolStr('True').parsed) - self.assertTrue(format.BoolStr('TRUE').parsed) - self.assertTrue(format.BoolStr('true').parsed) +class TestBoolStrFormatter(testtools.TestCase): - def test_parse_false(self): - self.assertFalse(format.BoolStr(False).parsed) - self.assertFalse(format.BoolStr('False').parsed) - self.assertFalse(format.BoolStr('FALSE').parsed) - self.assertFalse(format.BoolStr('false').parsed) + # NOTE: serialize/deserialize go through the same code path - def test_parse_fails(self): - self.assertRaises(ValueError, format.BoolStr, None) - self.assertRaises(ValueError, format.BoolStr, '') - self.assertRaises(ValueError, format.BoolStr, 'INVALID') + def test_format_true(self): + self.assertTrue(format.BoolStr.serialize(True)) + self.assertTrue(format.BoolStr.serialize('True')) + self.assertTrue(format.BoolStr.serialize('TRUE')) + self.assertTrue(format.BoolStr.serialize('true')) - def test_to_str_true(self): - self.assertEqual('True', str(format.BoolStr(True))) - self.assertEqual('True', str(format.BoolStr('True'))) + def test_format_false(self): + self.assertFalse(format.BoolStr.serialize(False)) + self.assertFalse(format.BoolStr.serialize('False')) + self.assertFalse(format.BoolStr.serialize('FALSE')) + self.assertFalse(format.BoolStr.serialize('false')) - def test_to_str_false(self): - self.assertEqual('False', str(format.BoolStr('False'))) - self.assertEqual('False', str(format.BoolStr(False))) + def test_format_fails(self): + self.assertRaises(ValueError, format.BoolStr.serialize, None) + self.assertRaises(ValueError, format.BoolStr.serialize, '') + self.assertRaises(ValueError, format.BoolStr.serialize, 'INVALID') + + +class TestISO8601Formatter(testtools.TestCase): + + def test_deserialize(self): + self.assertEqual( + format.ISO8601.deserialize("2015-06-27T05:09:43"), + datetime(2015, 6, 27, 5, 9, 43, tzinfo=iso8601.UTC)) + self.assertEqual( + format.ISO8601.deserialize("2012-03-28T21:31:02Z"), + datetime(2012, 3, 28, 21, 31, 2, tzinfo=iso8601.UTC)) + self.assertEqual( + format.ISO8601.deserialize("2013-09-23T13:53:12.774549"), + datetime(2013, 9, 23, 13, 53, 12, 774549, tzinfo=iso8601.UTC)) + self.assertEqual( + format.ISO8601.deserialize("2015-08-27T09:49:58-05:00"), + datetime(2015, 8, 27, 9, 49, 58, + tzinfo=iso8601.FixedOffset(-5, 0, "-05:00"))) + + def test_serialize(self): + self.assertEqual( + format.ISO8601.serialize( + datetime(2015, 6, 27, 5, 9, 43, tzinfo=iso8601.UTC)), + "2015-06-27T05:09:43+00:00") + self.assertEqual( + format.ISO8601.serialize( + datetime(2012, 3, 28, 21, 31, 2, tzinfo=iso8601.UTC)), + "2012-03-28T21:31:02+00:00") + self.assertEqual( + format.ISO8601.serialize( + datetime(2013, 9, 23, 13, 53, 12, 774549, tzinfo=iso8601.UTC)), + "2013-09-23T13:53:12.774549+00:00") + self.assertEqual( + format.ISO8601.serialize( + datetime(2015, 8, 27, 9, 49, 58, + tzinfo=iso8601.FixedOffset(-5, 0, "-05:00"))), + "2015-08-27T09:49:58-05:00") diff --git a/test-requirements.txt b/test-requirements.txt index 7ebd60b3..8e7a8f72 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -16,3 +16,4 @@ sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2 # BSD testrepository>=0.0.18 # Apache-2.0/BSD testscenarios>=0.4 # Apache-2.0/BSD testtools>=1.4.0 # MIT +iso8601>=0.1.9 # MIT