Merge "Add ISO8601 formatter as a type for properties"
This commit is contained in:
@@ -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)
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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)
|
@@ -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")
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user