Merge "Add ISO8601 formatter as a type for properties"

This commit is contained in:
Jenkins
2016-01-22 02:03:52 +00:00
committed by Gerrit Code Review
6 changed files with 124 additions and 68 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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")

View File

@@ -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