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
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import six
|
||||||
|
|
||||||
class BoolStr(object):
|
from oslo_utils import timeutils
|
||||||
"""A custom boolean/string hybrid type for resource.props.
|
|
||||||
|
|
||||||
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.
|
class Formatter(object):
|
||||||
A TypeError is raised when interpreted as neither True nor False.
|
|
||||||
"""
|
@classmethod
|
||||||
expr = str(given).lower()
|
def serialize(cls, value):
|
||||||
if 'true' == expr:
|
"""Return a string representing the formatted value"""
|
||||||
self.parsed = True
|
raise NotImplementedError
|
||||||
elif 'false' == expr:
|
|
||||||
self.parsed = False
|
@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:
|
else:
|
||||||
msg = 'Invalid as boolean: %s' % given
|
raise ValueError("Unable to serialize ISO8601: %s" % value)
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
def __bool__(self):
|
@classmethod
|
||||||
return self.parsed
|
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):
|
class BoolStr(Formatter):
|
||||||
return str(self.parsed)
|
|
||||||
|
# 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
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
from openstack import format
|
||||||
from openstack.metric import metric_service
|
from openstack.metric import metric_service
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
from openstack import resource_types
|
|
||||||
|
|
||||||
|
|
||||||
class Generic(resource.Resource):
|
class Generic(resource.Resource):
|
||||||
@@ -38,10 +38,10 @@ class Generic(resource.Resource):
|
|||||||
project_id = resource.prop('project_id')
|
project_id = resource.prop('project_id')
|
||||||
#: Timestamp when this resource was started
|
#: Timestamp when this resource was started
|
||||||
started_at = resource.prop('started_at',
|
started_at = resource.prop('started_at',
|
||||||
type=resource_types.ISO8601Datetime)
|
type=format.ISO8601)
|
||||||
#: Timestamp when this resource was ended
|
#: Timestamp when this resource was ended
|
||||||
ended_at = resource.prop('ended_at',
|
ended_at = resource.prop('ended_at',
|
||||||
type=resource_types.ISO8601Datetime)
|
type=format.ISO8601)
|
||||||
#: A dictionary of metrics collected on this resource
|
#: A dictionary of metrics collected on this resource
|
||||||
metrics = resource.prop('metrics', type=dict)
|
metrics = resource.prop('metrics', type=dict)
|
||||||
|
|
||||||
|
@@ -39,6 +39,7 @@ import six
|
|||||||
from six.moves.urllib import parse as url_parse
|
from six.moves.urllib import parse as url_parse
|
||||||
|
|
||||||
from openstack import exceptions
|
from openstack import exceptions
|
||||||
|
from openstack import format
|
||||||
from openstack import utils
|
from openstack import utils
|
||||||
|
|
||||||
|
|
||||||
@@ -120,11 +121,10 @@ class prop(object):
|
|||||||
value = self.type({self.type.id_attribute: value})
|
value = self.type({self.type.id_attribute: value})
|
||||||
else:
|
else:
|
||||||
value = self.type(value)
|
value = self.type(value)
|
||||||
|
elif issubclass(self.type, format.Formatter):
|
||||||
|
value = self.type.deserialize(value)
|
||||||
else:
|
else:
|
||||||
value = self.type(value)
|
value = self.type(value)
|
||||||
attr = getattr(value, 'parsed', None)
|
|
||||||
if attr is not None:
|
|
||||||
value = attr
|
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -136,6 +136,8 @@ class prop(object):
|
|||||||
value = self.type({self.type.id_attribute: value})
|
value = self.type({self.type.id_attribute: value})
|
||||||
else:
|
else:
|
||||||
value = self.type(value)
|
value = self.type(value)
|
||||||
|
elif issubclass(self.type, format.Formatter):
|
||||||
|
value = self.type.serialize(value)
|
||||||
else:
|
else:
|
||||||
value = str(self.type(value)) # validate to fail fast
|
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
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from iso8601 import iso8601
|
||||||
import testtools
|
import testtools
|
||||||
|
|
||||||
from openstack import format
|
from openstack import format
|
||||||
|
|
||||||
|
|
||||||
class TestFormat(testtools.TestCase):
|
class TestBoolStrFormatter(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)
|
|
||||||
|
|
||||||
def test_parse_false(self):
|
# NOTE: serialize/deserialize go through the same code path
|
||||||
self.assertFalse(format.BoolStr(False).parsed)
|
|
||||||
self.assertFalse(format.BoolStr('False').parsed)
|
|
||||||
self.assertFalse(format.BoolStr('FALSE').parsed)
|
|
||||||
self.assertFalse(format.BoolStr('false').parsed)
|
|
||||||
|
|
||||||
def test_parse_fails(self):
|
def test_format_true(self):
|
||||||
self.assertRaises(ValueError, format.BoolStr, None)
|
self.assertTrue(format.BoolStr.serialize(True))
|
||||||
self.assertRaises(ValueError, format.BoolStr, '')
|
self.assertTrue(format.BoolStr.serialize('True'))
|
||||||
self.assertRaises(ValueError, format.BoolStr, 'INVALID')
|
self.assertTrue(format.BoolStr.serialize('TRUE'))
|
||||||
|
self.assertTrue(format.BoolStr.serialize('true'))
|
||||||
|
|
||||||
def test_to_str_true(self):
|
def test_format_false(self):
|
||||||
self.assertEqual('True', str(format.BoolStr(True)))
|
self.assertFalse(format.BoolStr.serialize(False))
|
||||||
self.assertEqual('True', str(format.BoolStr('True')))
|
self.assertFalse(format.BoolStr.serialize('False'))
|
||||||
|
self.assertFalse(format.BoolStr.serialize('FALSE'))
|
||||||
|
self.assertFalse(format.BoolStr.serialize('false'))
|
||||||
|
|
||||||
def test_to_str_false(self):
|
def test_format_fails(self):
|
||||||
self.assertEqual('False', str(format.BoolStr('False')))
|
self.assertRaises(ValueError, format.BoolStr.serialize, None)
|
||||||
self.assertEqual('False', str(format.BoolStr(False)))
|
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
|
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||||
testscenarios>=0.4 # Apache-2.0/BSD
|
testscenarios>=0.4 # Apache-2.0/BSD
|
||||||
testtools>=1.4.0 # MIT
|
testtools>=1.4.0 # MIT
|
||||||
|
iso8601>=0.1.9 # MIT
|
||||||
|
Reference in New Issue
Block a user