Add UNIXEpoch formatter as a type for properties
Several properties in the object_store service use timestamp values that are a count of seconds since the UNIX Epoch, or January 1, 1970. This change adds a formatter that exposes this value to the user as a datetime object, similar to the ISO8601 formatter. It is currently in use by the transaction timestamp X-Timestamp header on all object_store resources as well as the X-Delete-At header value on objects. Change-Id: I3a14e3fae0e22d1e1ae411a5b119a08c6602c2b9
This commit is contained in:
		@@ -11,6 +11,10 @@
 | 
				
			|||||||
# under the License.
 | 
					# under the License.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from datetime import datetime
 | 
					from datetime import datetime
 | 
				
			||||||
 | 
					import numbers
 | 
				
			||||||
 | 
					import time
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from iso8601 import iso8601
 | 
				
			||||||
import six
 | 
					import six
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from oslo_utils import timeutils
 | 
					from oslo_utils import timeutils
 | 
				
			||||||
@@ -54,6 +58,57 @@ class ISO8601(Formatter):
 | 
				
			|||||||
            raise ValueError("Unable to deserialize ISO8601: %s" % value)
 | 
					            raise ValueError("Unable to deserialize ISO8601: %s" % value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class UNIXEpoch(Formatter):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    EPOCH = datetime(1970, 1, 1, tzinfo=iso8601.UTC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def serialize(cls, value):
 | 
				
			||||||
 | 
					        """Convert a datetime to a UNIX epoch"""
 | 
				
			||||||
 | 
					        if isinstance(value, datetime):
 | 
				
			||||||
 | 
					            # Do not try to format using %s as it's platform dependent.
 | 
				
			||||||
 | 
					            return (value - cls.EPOCH).total_seconds()
 | 
				
			||||||
 | 
					        elif isinstance(value, numbers.Number):
 | 
				
			||||||
 | 
					            return value
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            raise ValueError("Unable to serialize UNIX epoch: %s" % value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def deserialize(cls, value):
 | 
				
			||||||
 | 
					        """Convert a UNIX epoch into a datetime object"""
 | 
				
			||||||
 | 
					        try:
 | 
				
			||||||
 | 
					            value = float(value)
 | 
				
			||||||
 | 
					        except ValueError:
 | 
				
			||||||
 | 
					            raise ValueError("Unable to deserialize UNIX epoch: %s" % value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # gmtime doesn't respect microseconds so we need to parse them out
 | 
				
			||||||
 | 
					        # if they're there and build the datetime appropriately with the
 | 
				
			||||||
 | 
					        # proper precision.
 | 
				
			||||||
 | 
					        # NOTES:
 | 
				
			||||||
 | 
					        # 1. datetime.fromtimestamp sort of solves this but using localtime
 | 
				
			||||||
 | 
					        #    instead of UTC, which we need.
 | 
				
			||||||
 | 
					        # 2. On Python 2 we can't just str(value) as it truncates digits
 | 
				
			||||||
 | 
					        #    that are significant to us.
 | 
				
			||||||
 | 
					        parsed_value = "%000000f" % value
 | 
				
			||||||
 | 
					        decimal = parsed_value.find(".")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if decimal == -1:
 | 
				
			||||||
 | 
					            microsecond = 0
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            # Some examples of these timestamps include less precision
 | 
				
			||||||
 | 
					            # than the allowable 6 digits that can represent microseconds,
 | 
				
			||||||
 | 
					            # so since we have a string we need to construct a real
 | 
				
			||||||
 | 
					            # count of microseconds instead of just converting the
 | 
				
			||||||
 | 
					            # stringified amount to an int.
 | 
				
			||||||
 | 
					            fractional_second = float(parsed_value[decimal:]) * 1e6
 | 
				
			||||||
 | 
					            microsecond = int(fractional_second)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        gmt = time.gmtime(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return datetime(*gmt[:6], microsecond=microsecond,
 | 
				
			||||||
 | 
					                        tzinfo=iso8601.UTC)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class BoolStr(Formatter):
 | 
					class BoolStr(Formatter):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # The behavior here primarily exists for the deserialize method
 | 
					    # The behavior here primarily exists for the deserialize method
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
# 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.object_store.v1 import _base
 | 
					from openstack.object_store.v1 import _base
 | 
				
			||||||
from openstack import resource
 | 
					from openstack import resource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,3 +37,5 @@ class Account(_base.BaseResource):
 | 
				
			|||||||
    #: A second secret key value for temporary URLs. If not set,
 | 
					    #: A second secret key value for temporary URLs. If not set,
 | 
				
			||||||
    #: this header is not returned by this operation.
 | 
					    #: this header is not returned by this operation.
 | 
				
			||||||
    meta_temp_url_key_2 = resource.header("x-account-meta-temp-url-key-2")
 | 
					    meta_temp_url_key_2 = resource.header("x-account-meta-temp-url-key-2")
 | 
				
			||||||
 | 
					    #: The timestamp of the transaction.
 | 
				
			||||||
 | 
					    timestamp = resource.header("x-timestamp", type=format.UNIXEpoch)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
# 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.object_store.v1 import _base
 | 
					from openstack.object_store.v1 import _base
 | 
				
			||||||
from openstack import resource
 | 
					from openstack import resource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,6 +41,8 @@ class Container(_base.BaseResource):
 | 
				
			|||||||
    object_count = resource.header("x-container-object-count", type=int)
 | 
					    object_count = resource.header("x-container-object-count", type=int)
 | 
				
			||||||
    #: The count of bytes used in total.
 | 
					    #: The count of bytes used in total.
 | 
				
			||||||
    bytes_used = resource.header("x-container-bytes-used", type=int)
 | 
					    bytes_used = resource.header("x-container-bytes-used", type=int)
 | 
				
			||||||
 | 
					    #: The timestamp of the transaction.
 | 
				
			||||||
 | 
					    timestamp = resource.header("x-timestamp", type=format.UNIXEpoch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Request headers (when id=None)
 | 
					    # Request headers (when id=None)
 | 
				
			||||||
    #: If set to True, Object Storage queries all replicas to return the
 | 
					    #: If set to True, Object Storage queries all replicas to return the
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@
 | 
				
			|||||||
# 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.object_store import object_store_service
 | 
					from openstack.object_store import object_store_service
 | 
				
			||||||
from openstack import resource
 | 
					from openstack import resource
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -117,13 +118,13 @@ class Object(resource.Resource):
 | 
				
			|||||||
    #: If set, the time when the object will be deleted by the system
 | 
					    #: If set, the time when the object will be deleted by the system
 | 
				
			||||||
    #: in the format of a UNIX Epoch timestamp.
 | 
					    #: in the format of a UNIX Epoch timestamp.
 | 
				
			||||||
    #: If not set, this header is not returned by this operation.
 | 
					    #: If not set, this header is not returned by this operation.
 | 
				
			||||||
    delete_at = resource.header("x-delete-at", type=int)
 | 
					    delete_at = resource.header("x-delete-at", type=format.UNIXEpoch)
 | 
				
			||||||
    #: If set, to this is a dynamic large object manifest object.
 | 
					    #: If set, to this is a dynamic large object manifest object.
 | 
				
			||||||
    #: The value is the container and object name prefix of the
 | 
					    #: The value is the container and object name prefix of the
 | 
				
			||||||
    #: segment objects in the form container/prefix.
 | 
					    #: segment objects in the form container/prefix.
 | 
				
			||||||
    object_manifest = resource.header("x-object-manifest")
 | 
					    object_manifest = resource.header("x-object-manifest")
 | 
				
			||||||
    #: The UNIX timestamp of the transaction.
 | 
					    #: The timestamp of the transaction.
 | 
				
			||||||
    timestamp = resource.header("x-timestamp")
 | 
					    timestamp = resource.header("x-timestamp", type=format.UNIXEpoch)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Headers for PUT and POST requests
 | 
					    # Headers for PUT and POST requests
 | 
				
			||||||
    #: Set to chunked to enable chunked transfer encoding. If used,
 | 
					    #: Set to chunked to enable chunked transfer encoding. If used,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,9 @@
 | 
				
			|||||||
# 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 iso8601
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import testtools
 | 
					import testtools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from openstack.object_store.v1 import account
 | 
					from openstack.object_store.v1 import account
 | 
				
			||||||
@@ -24,7 +27,8 @@ ACCOUNT_EXAMPLE = {
 | 
				
			|||||||
    'x-account-bytes-used': '12345',
 | 
					    'x-account-bytes-used': '12345',
 | 
				
			||||||
    'x-account-container-count': '678',
 | 
					    'x-account-container-count': '678',
 | 
				
			||||||
    'content-type': 'text/plain; charset=utf-8',
 | 
					    'content-type': 'text/plain; charset=utf-8',
 | 
				
			||||||
    'x-account-object-count': '98765'
 | 
					    'x-account-object-count': '98765',
 | 
				
			||||||
 | 
					    'x-timestamp': '1453413555.88937'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,3 +56,6 @@ class TestAccount(testtools.TestCase):
 | 
				
			|||||||
                         sot.account_container_count)
 | 
					                         sot.account_container_count)
 | 
				
			||||||
        self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-object-count']),
 | 
					        self.assertEqual(int(ACCOUNT_EXAMPLE['x-account-object-count']),
 | 
				
			||||||
                         sot.account_object_count)
 | 
					                         sot.account_object_count)
 | 
				
			||||||
 | 
					        self.assertEqual(datetime(2016, 1, 21, 21, 59, 15, 889370,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC),
 | 
				
			||||||
 | 
					                         sot.timestamp)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,9 @@
 | 
				
			|||||||
# 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 iso8601
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import mock
 | 
					import mock
 | 
				
			||||||
import testtools
 | 
					import testtools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,7 +39,8 @@ HEAD_EXAMPLE = {
 | 
				
			|||||||
    'x-container-sync-key': 'sync-key',
 | 
					    'x-container-sync-key': 'sync-key',
 | 
				
			||||||
    'x-container-bytes-used': '630666',
 | 
					    'x-container-bytes-used': '630666',
 | 
				
			||||||
    'x-versions-location': 'versions-location',
 | 
					    'x-versions-location': 'versions-location',
 | 
				
			||||||
    'content-type': 'application/json; charset=utf-8'
 | 
					    'content-type': 'application/json; charset=utf-8',
 | 
				
			||||||
 | 
					    'x-timestamp': '1453414055.48672'
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LIST_EXAMPLE = [
 | 
					LIST_EXAMPLE = [
 | 
				
			||||||
@@ -112,6 +116,9 @@ class TestContainer(testtools.TestCase):
 | 
				
			|||||||
                         sot.sync_key)
 | 
					                         sot.sync_key)
 | 
				
			||||||
        self.assertEqual(HEAD_EXAMPLE['x-versions-location'],
 | 
					        self.assertEqual(HEAD_EXAMPLE['x-versions-location'],
 | 
				
			||||||
                         sot.versions_location)
 | 
					                         sot.versions_location)
 | 
				
			||||||
 | 
					        self.assertEqual(datetime(2016, 1, 21, 22, 7, 35, 486720,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC),
 | 
				
			||||||
 | 
					                         sot.timestamp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @mock.patch("openstack.resource.Resource.list")
 | 
					    @mock.patch("openstack.resource.Resource.list")
 | 
				
			||||||
    def test_list(self, fake_list):
 | 
					    def test_list(self, fake_list):
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,6 +10,9 @@
 | 
				
			|||||||
# 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 iso8601
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import mock
 | 
					import mock
 | 
				
			||||||
import testtools
 | 
					import testtools
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,10 +49,11 @@ DICT_EXAMPLE = {
 | 
				
			|||||||
        'accept-ranges': 'bytes',
 | 
					        'accept-ranges': 'bytes',
 | 
				
			||||||
        'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT',
 | 
					        'last-modified': 'Sun, 13 Jul 2014 18:41:04 GMT',
 | 
				
			||||||
        'etag': '243f87b91224d85722564a80fd3cb1f1',
 | 
					        'etag': '243f87b91224d85722564a80fd3cb1f1',
 | 
				
			||||||
        'x-timestamp': '1405276863.31924',
 | 
					        'x-timestamp': '1453414256.28112',
 | 
				
			||||||
        'date': 'Thu, 28 Aug 2014 14:41:59 GMT',
 | 
					        'date': 'Thu, 28 Aug 2014 14:41:59 GMT',
 | 
				
			||||||
        'content-type': 'application/octet-stream',
 | 
					        'content-type': 'application/octet-stream',
 | 
				
			||||||
        'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7'
 | 
					        'id': 'tx5fb5ad4f4d0846c6b2bc7-0053ff3fb7',
 | 
				
			||||||
 | 
					        'x-delete-at': '1453416226.16744'
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -94,9 +98,14 @@ class TestObject(testtools.TestCase):
 | 
				
			|||||||
        self.assertEqual(headers['accept-ranges'], sot.accept_ranges)
 | 
					        self.assertEqual(headers['accept-ranges'], sot.accept_ranges)
 | 
				
			||||||
        self.assertEqual(headers['last-modified'], sot.last_modified)
 | 
					        self.assertEqual(headers['last-modified'], sot.last_modified)
 | 
				
			||||||
        self.assertEqual(headers['etag'], sot.etag)
 | 
					        self.assertEqual(headers['etag'], sot.etag)
 | 
				
			||||||
        self.assertEqual(headers['x-timestamp'], sot.timestamp)
 | 
					        self.assertEqual(datetime(2016, 1, 21, 22, 10, 56, 281120,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC),
 | 
				
			||||||
 | 
					                         sot.timestamp)
 | 
				
			||||||
        self.assertEqual(headers['date'], sot.date)
 | 
					        self.assertEqual(headers['date'], sot.date)
 | 
				
			||||||
        self.assertEqual(headers['content-type'], sot.content_type)
 | 
					        self.assertEqual(headers['content-type'], sot.content_type)
 | 
				
			||||||
 | 
					        self.assertEqual(datetime(2016, 1, 21, 22, 43, 46, 167440,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC),
 | 
				
			||||||
 | 
					                         sot.delete_at)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_get(self):
 | 
					    def test_get(self):
 | 
				
			||||||
        sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
 | 
					        sot = obj.Object.new(container=CONTAINER_NAME, name=OBJECT_NAME)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -74,3 +74,32 @@ class TestISO8601Formatter(testtools.TestCase):
 | 
				
			|||||||
                datetime(2015, 8, 27, 9, 49, 58,
 | 
					                datetime(2015, 8, 27, 9, 49, 58,
 | 
				
			||||||
                         tzinfo=iso8601.FixedOffset(-5, 0, "-05:00"))),
 | 
					                         tzinfo=iso8601.FixedOffset(-5, 0, "-05:00"))),
 | 
				
			||||||
            "2015-08-27T09:49:58-05:00")
 | 
					            "2015-08-27T09:49:58-05:00")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestUNIXEpochFormatter(testtools.TestCase):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_deserialize(self):
 | 
				
			||||||
 | 
					        self.assertEqual(format.UNIXEpoch.deserialize(1453412616.02406),
 | 
				
			||||||
 | 
					                         datetime(2016, 1, 21, 21, 43, 36, 24060,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC))
 | 
				
			||||||
 | 
					        self.assertEqual(format.UNIXEpoch.deserialize(1389453423.35964),
 | 
				
			||||||
 | 
					                         datetime(2014, 1, 11, 15, 17, 3, 359640,
 | 
				
			||||||
 | 
					                                  tzinfo=iso8601.UTC))
 | 
				
			||||||
 | 
					        self.assertEqual(format.UNIXEpoch.deserialize(1389453423),
 | 
				
			||||||
 | 
					                         datetime(2014, 1, 11, 15, 17, 3, tzinfo=iso8601.UTC))
 | 
				
			||||||
 | 
					        self.assertRaises(ValueError, format.UNIXEpoch.deserialize, "lol")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_serialize(self):
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            format.UNIXEpoch.serialize(
 | 
				
			||||||
 | 
					                datetime(2016, 1, 21, 21, 43, 36, 24060, tzinfo=iso8601.UTC)),
 | 
				
			||||||
 | 
					            1453412616.02406)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            format.UNIXEpoch.serialize(
 | 
				
			||||||
 | 
					                datetime(2014, 1, 11, 15, 17, 3, 359640, tzinfo=iso8601.UTC)),
 | 
				
			||||||
 | 
					            1389453423.35964)
 | 
				
			||||||
 | 
					        self.assertEqual(
 | 
				
			||||||
 | 
					            format.UNIXEpoch.serialize(
 | 
				
			||||||
 | 
					                datetime(2014, 1, 11, 15, 17, 3, tzinfo=iso8601.UTC)),
 | 
				
			||||||
 | 
					            1389453423)
 | 
				
			||||||
 | 
					        self.assertRaises(ValueError, format.UNIXEpoch.serialize, "lol")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,3 +7,4 @@ stevedore>=1.5.0 # Apache-2.0
 | 
				
			|||||||
oslo.utils>=3.4.0 # Apache-2.0
 | 
					oslo.utils>=3.4.0 # Apache-2.0
 | 
				
			||||||
os-client-config>=1.13.1 # Apache-2.0
 | 
					os-client-config>=1.13.1 # Apache-2.0
 | 
				
			||||||
keystoneauth1>=2.1.0 # Apache-2.0
 | 
					keystoneauth1>=2.1.0 # Apache-2.0
 | 
				
			||||||
 | 
					iso8601>=0.1.9 # MIT
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,4 +16,3 @@ 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