Add ISO8601 formatter as a type for properties
While thinking about the type=Resource changes that we're undergoing, a common type that we could be improving for users are timestamps. If a user is going to use a timestamp for anything -- the common ones being the timestamp of when a resource was created or updated -- they're going to need to convert it to a datetime anyway in order to look into it. By far the most common timestamp format coming back in responses and being accepted in requests is an ISO 8601 string. We had previously been depending on the oslo_utils.timeutils library to handle a form of this type of behavior for the metric resource, but it was limited in its functionality as well as only being used by metric. This approach makes the idea more generally useful. Additionaly, this approach can be applied to the BoolStr class which had previously existed to handle the case of some services returning "true" and "false" in responses. This required a special case inside the prop code to look for a "parsed" attribute. Changing the BoolStr class to work with the Formatter base allows both BoolStr and ISO8601 to use the same code paths. This change doesn't make any wholesale changes to implement type=format.ISO8601. Those should be handled in a separate change once the implementation is approved. Change-Id: I37e99afcf3a0eca7c806e9dea984f3059f677ec4
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