From 1d2b3e36118da4ded1624e3448adf357eb7a1b56 Mon Sep 17 00:00:00 2001 From: Andrew Hawker Date: Mon, 17 Aug 2020 14:36:00 -0700 Subject: [PATCH] Add monotonic randomness support Doing this required a refactoring of how timestamp/randomness values were generated. We've broken these into "provider" implementations with the "default" provider being the implementation that exists today, e.g. randomness values are random even on identical timestamp values. A "monotonic" provider has been added which monotonically increments the first randomness value on timestamp collision until an overflow. Additionally, the API has been broken out into a subpackage so we can stay agnostic to the provider and just plug it in. Work has been done to maintain the existing package interface for backwards compatibility. --- tests/test_module.py | 7 +- ulid/__init__.py | 40 +++--- ulid/api.py | 255 ----------------------------------- ulid/api/__init__.py | 31 +++++ ulid/api/api.py | 261 ++++++++++++++++++++++++++++++++++++ ulid/api/default.py | 33 +++++ ulid/api/monotonic.py | 33 +++++ ulid/consts.py | 32 +++++ ulid/hints.py | 9 +- ulid/providers/__init__.py | 14 ++ ulid/providers/base.py | 35 +++++ ulid/providers/default.py | 35 +++++ ulid/providers/monotonic.py | 57 ++++++++ 13 files changed, 561 insertions(+), 281 deletions(-) delete mode 100644 ulid/api.py create mode 100644 ulid/api/__init__.py create mode 100644 ulid/api/api.py create mode 100644 ulid/api/default.py create mode 100644 ulid/api/monotonic.py create mode 100644 ulid/consts.py create mode 100644 ulid/providers/__init__.py create mode 100644 ulid/providers/base.py create mode 100644 ulid/providers/default.py create mode 100644 ulid/providers/monotonic.py diff --git a/tests/test_module.py b/tests/test_module.py index 28e9b1a..fe94a85 100644 --- a/tests/test_module.py +++ b/tests/test_module.py @@ -37,9 +37,8 @@ def test_module_has_submodule_interface(submodule): assert hasattr(mod, i) -def test_module_exposes_api_and_ulid_interfaces_via_all(): +def test_module_exposes_api_interfaces_via_all(): """ - Assert that :mod:`~ulid` exposes the :attr:`~ulid.api.__all__` and :attr:`~ulid.ulid.__all__` - attributes in its public interface. + Assert that :mod:`~ulid` exposes the :attr:`~ulid.api.__all__` attributes in its public interface. """ - assert mod.__all__ == api.__all__ + ulid.__all__ + assert mod.__all__ == api.__all__ diff --git a/ulid/__init__.py b/ulid/__init__.py index 65cde5c..e6edc9f 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -7,29 +7,29 @@ :copyright: (c) 2017 Andrew Hawker. :license: Apache 2.0, see LICENSE for more details. """ -from . import api, ulid +from .api import default, monotonic -create = api.create -from_bytes = api.from_bytes -from_int = api.from_int -from_randomness = api.from_randomness -from_str = api.from_str -from_timestamp = api.from_timestamp -from_uuid = api.from_uuid -new = api.new -parse = api.parse +create = default.create +from_bytes = default.from_bytes +from_int = default.from_int +from_randomness = default.from_randomness +from_str = default.from_str +from_timestamp = default.from_timestamp +from_uuid = default.from_uuid +new = default.new +parse = default.parse -MIN_TIMESTAMP = api.MIN_TIMESTAMP -MAX_TIMESTAMP = api.MAX_TIMESTAMP -MIN_RANDOMNESS = api.MIN_RANDOMNESS -MAX_RANDOMNESS = api.MAX_RANDOMNESS -MIN_ULID = api.MIN_ULID -MAX_ULID = api.MAX_ULID +MIN_TIMESTAMP = default.MIN_TIMESTAMP +MAX_TIMESTAMP = default.MAX_TIMESTAMP +MIN_RANDOMNESS = default.MIN_RANDOMNESS +MAX_RANDOMNESS = default.MAX_RANDOMNESS +MIN_ULID = default.MIN_ULID +MAX_ULID = default.MAX_ULID -Timestamp = ulid.Timestamp -Randomness = ulid.Randomness -ULID = ulid.ULID +Timestamp = default.Timestamp +Randomness = default.Randomness +ULID = default.ULID -__all__ = api.__all__ + ulid.__all__ +__all__ = default.__all__ __version__ = '0.2.0' diff --git a/ulid/api.py b/ulid/api.py deleted file mode 100644 index bc24bcb..0000000 --- a/ulid/api.py +++ /dev/null @@ -1,255 +0,0 @@ -""" - ulid/api - ~~~~~~~~ - - Defines the public API of the `ulid` package. -""" -import os -import time -import typing -import uuid - -from . import base32, codec, hints, ulid - -__all__ = ['new', 'parse', 'create', 'from_bytes', 'from_int', 'from_str', - 'from_uuid', 'from_timestamp', 'from_randomness', - 'MIN_TIMESTAMP', 'MAX_TIMESTAMP', 'MIN_RANDOMNESS', 'MAX_RANDOMNESS', 'MIN_ULID', 'MAX_ULID'] - -#: Minimum possible timestamp value (0). -MIN_TIMESTAMP = ulid.Timestamp(b'\x00\x00\x00\x00\x00\x00') - - -#: Maximum possible timestamp value (281474976710.655 epoch). -MAX_TIMESTAMP = ulid.Timestamp(b'\xff\xff\xff\xff\xff\xff') - - -#: Minimum possible randomness value (0). -MIN_RANDOMNESS = ulid.Randomness(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') - - -#: Maximum possible randomness value (1208925819614629174706175). -MAX_RANDOMNESS = ulid.Randomness(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff') - - -#: Minimum possible ULID value (0). -MIN_ULID = ulid.ULID(MIN_TIMESTAMP.bytes + MIN_RANDOMNESS.bytes) - - -#: Maximum possible ULID value (340282366920938463463374607431768211455). -MAX_ULID = ulid.ULID(MAX_TIMESTAMP.bytes + MAX_RANDOMNESS.bytes) - - -#: Type hint that defines multiple primitive types that can represent a full ULID. -ULIDPrimitive = typing.Union[hints.Primitive, uuid.UUID, ulid.ULID] # pylint: disable=invalid-name - - -def new() -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance. - - The timestamp is created from :func:`~time.time`. - The randomness is created from :func:`~os.urandom`. - - :return: ULID from current timestamp - :rtype: :class:`~ulid.ulid.ULID` - """ - timestamp = int(time.time() * 1000).to_bytes(6, byteorder='big') - randomness = os.urandom(10) - return ulid.ULID(timestamp + randomness) - - -def parse(value: ULIDPrimitive) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance from the given value. - - .. note:: This method should only be used when the caller is trying to parse a ULID from - a value when they're unsure what format/primitive type it will be given in. - - :param value: ULID value of any supported type - :type value: :class:`~ulid.api.ULIDPrimitive` - :return: ULID from value - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when unable to parse a ULID from the value - """ - if isinstance(value, ulid.ULID): - return value - if isinstance(value, uuid.UUID): - return from_uuid(value) - if isinstance(value, str): - len_value = len(value) - if len_value == 36: - return from_uuid(uuid.UUID(value)) - if len_value == 32: - return from_uuid(uuid.UUID(value)) - if len_value == 26: - return from_str(value) - if len_value == 16: - return from_randomness(value) - if len_value == 10: - return from_timestamp(value) - raise ValueError('Cannot create ULID from string of length {}'.format(len_value)) - if isinstance(value, (int, float)): - return from_int(int(value)) - if isinstance(value, (bytes, bytearray)): - return from_bytes(value) - if isinstance(value, memoryview): - return from_bytes(value.tobytes()) - raise ValueError('Cannot create ULID from type {}'.format(value.__class__.__name__)) - - -def create(timestamp: codec.TimestampPrimitive, randomness: codec.RandomnessPrimitive) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance using the given timestamp and randomness values. - - The following types are supported for timestamp values: - - * :class:`~datetime.datetime` - * :class:`~int` - * :class:`~float` - * :class:`~str` - * :class:`~memoryview` - * :class:`~ulid.ulid.Timestamp` - * :class:`~ulid.ulid.ULID` - * :class:`~bytes` - * :class:`~bytearray` - - The following types are supported for randomness values: - - * :class:`~int` - * :class:`~float` - * :class:`~str` - * :class:`~memoryview` - * :class:`~ulid.ulid.Randomness` - * :class:`~ulid.ulid.ULID` - * :class:`~bytes` - * :class:`~bytearray` - - :param timestamp: Unix timestamp in seconds - :type timestamp: See docstring for types - :param randomness: Random bytes - :type randomness: See docstring for types - :return: ULID using given timestamp and randomness - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when a value is an unsupported type - :raises ValueError: when a value is a string and cannot be Base32 decoded - :raises ValueError: when a value is or was converted to incorrect bit length - """ - timestamp = codec.decode_timestamp(timestamp) - randomness = codec.decode_randomness(randomness) - return ulid.ULID(timestamp.bytes + randomness.bytes) - - -def from_bytes(value: hints.Buffer) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~bytes`, - :class:`~bytearray`, or :class:`~memoryview` value. - - :param value: 16 bytes - :type value: :class:`~bytes`, :class:`~bytearray`, or :class:`~memoryview` - :return: ULID from buffer value - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when the value is not 16 bytes - """ - length = len(value) - if length != 16: - raise ValueError('Expects bytes to be 128 bits; got {} bytes'.format(length)) - - return ulid.ULID(value) - - -def from_int(value: int) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~int` value. - - :param value: 128 bit integer - :type value: :class:`~int` - :return: ULID from integer value - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when the value is not a 128 bit integer - """ - if value < 0: - raise ValueError('Expects positive integer') - - length = (value.bit_length() + 7) // 8 - if length > 16: - raise ValueError('Expects integer to be 128 bits; got {} bytes'.format(length)) - - return ulid.ULID(value.to_bytes(16, byteorder='big')) - - -def from_str(value: str) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` value. - - :param value: Base32 encoded string - :type value: :class:`~str` - :return: ULID from string value - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when the value is not 26 characters or malformed - """ - return ulid.ULID(base32.decode_ulid(value)) - - -def from_uuid(value: uuid.UUID) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~uuid.UUID` value. - - :param value: UUIDv4 value - :type value: :class:`~uuid.UUID` - :return: ULID from UUID value - :rtype: :class:`~ulid.ulid.ULID` - """ - return ulid.ULID(value.bytes) - - -def from_timestamp(timestamp: codec.TimestampPrimitive) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance using a timestamp value of a supported type. - - The following types are supported for timestamp values: - - * :class:`~datetime.datetime` - * :class:`~int` - * :class:`~float` - * :class:`~str` - * :class:`~memoryview` - * :class:`~ulid.ulid.Timestamp` - * :class:`~ulid.ulid.ULID` - * :class:`~bytes` - * :class:`~bytearray` - - :param timestamp: Unix timestamp in seconds - :type timestamp: See docstring for types - :return: ULID using given timestamp and new randomness - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when the value is an unsupported type - :raises ValueError: when the value is a string and cannot be Base32 decoded - :raises ValueError: when the value is or was converted to something 48 bits - """ - return create(timestamp, os.urandom(10)) - - -def from_randomness(randomness: codec.RandomnessPrimitive) -> ulid.ULID: - """ - Create a new :class:`~ulid.ulid.ULID` instance using the given randomness value of a supported type. - - The following types are supported for randomness values: - - * :class:`~int` - * :class:`~float` - * :class:`~str` - * :class:`~memoryview` - * :class:`~ulid.ulid.Randomness` - * :class:`~ulid.ulid.ULID` - * :class:`~bytes` - * :class:`~bytearray` - - :param randomness: Random bytes - :type randomness: See docstring for types - :return: ULID using new timestamp and given randomness - :rtype: :class:`~ulid.ulid.ULID` - :raises ValueError: when the value is an unsupported type - :raises ValueError: when the value is a string and cannot be Base32 decoded - :raises ValueError: when the value is or was converted to something 80 bits - """ - return create(int(time.time() * 1000).to_bytes(6, byteorder='big'), randomness) diff --git a/ulid/api/__init__.py b/ulid/api/__init__.py new file mode 100644 index 0000000..6e22615 --- /dev/null +++ b/ulid/api/__init__.py @@ -0,0 +1,31 @@ +""" + ulid/api + ~~~~~~~~ + + Defines the public API of the `ulid` package. +""" +from .. import consts, ulid +from . import default + +create = default.create +from_bytes = default.from_bytes +from_int = default.from_int +from_randomness = default.from_randomness +from_str = default.from_str +from_timestamp = default.from_timestamp +from_uuid = default.from_uuid +new = default.new +parse = default.parse + +MIN_TIMESTAMP = consts.MIN_TIMESTAMP +MAX_TIMESTAMP = consts.MAX_TIMESTAMP +MIN_RANDOMNESS = consts.MIN_RANDOMNESS +MAX_RANDOMNESS = consts.MAX_RANDOMNESS +MIN_ULID = consts.MIN_ULID +MAX_ULID = consts.MAX_ULID + +Timestamp = ulid.Timestamp +Randomness = ulid.Randomness +ULID = ulid.ULID + +__all__ = default.__all__ diff --git a/ulid/api/api.py b/ulid/api/api.py new file mode 100644 index 0000000..72727ee --- /dev/null +++ b/ulid/api/api.py @@ -0,0 +1,261 @@ +""" + ulid/api/api + ~~~~~~~~~~~~ + + Contains functionality for public API methods for the 'ulid' package. +""" +import typing +import uuid + +from .. import base32, codec, hints, providers, ulid + +#: Type hint that defines multiple primitive types that can represent a full ULID. +ULIDPrimitive = typing.Union[hints.Primitive, uuid.UUID, ulid.ULID] # pylint: disable=invalid-name + +#: Defines the '__all__' for the API interface. +ALL = [ + 'new', + 'parse', + 'create', + 'from_bytes', + 'from_int', + 'from_str', + 'from_uuid', + 'from_timestamp', + 'from_randomness', + 'MIN_TIMESTAMP', + 'MAX_TIMESTAMP', + 'MIN_RANDOMNESS', + 'MAX_RANDOMNESS', + 'MIN_ULID', + 'MAX_ULID', + 'Timestamp', + 'Randomness', + 'ULID' +] + + +class Api: + """ + Encapsulates public API methods for the 'ulid' package that is agnostic to the underlying + timestamp/randomness provider. + """ + def __init__(self, provider: providers.Provider) -> None: + """ + Create a new API instance with the given provider. + + :param provider: Provider that yields timestamp/randomness values. + :type provider: :class:`~ulid.providers.Provider` + """ + self.provider = provider + + def new(self) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance. + + The timestamp and randomness values are created from + the instance :class:`~ulid.providers.Provider`. + + :return: ULID from current timestamp + :rtype: :class:`~ulid.ulid.ULID` + """ + timestamp = self.provider.timestamp() + randomness = self.provider.randomness(timestamp) + return ulid.ULID(timestamp + randomness) + + def parse(self, value: ULIDPrimitive) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance from the given value. + + .. note:: This method should only be used when the caller is trying to parse a ULID from + a value when they're unsure what format/primitive type it will be given in. + + :param value: ULID value of any supported type + :type value: :class:`~ulid.api.ULIDPrimitive` + :return: ULID from value + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when unable to parse a ULID from the value + """ + if isinstance(value, ulid.ULID): + return value + if isinstance(value, uuid.UUID): + return self.from_uuid(value) + if isinstance(value, str): + len_value = len(value) + if len_value == 36: + return self.from_uuid(uuid.UUID(value)) + if len_value == 32: + return self.from_uuid(uuid.UUID(value)) + if len_value == 26: + return self.from_str(value) + if len_value == 16: + return self.from_randomness(value) + if len_value == 10: + return self.from_timestamp(value) + raise ValueError('Cannot create ULID from string of length {}'.format(len_value)) + if isinstance(value, (int, float)): + return self.from_int(int(value)) + if isinstance(value, (bytes, bytearray)): + return self.from_bytes(value) + if isinstance(value, memoryview): + return self.from_bytes(value.tobytes()) + raise ValueError('Cannot create ULID from type {}'.format(value.__class__.__name__)) + + def from_timestamp(self, timestamp: codec.TimestampPrimitive) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance using a timestamp value of a supported type. + + The following types are supported for timestamp values: + + * :class:`~datetime.datetime` + * :class:`~int` + * :class:`~float` + * :class:`~str` + * :class:`~memoryview` + * :class:`~ulid.ulid.Timestamp` + * :class:`~ulid.ulid.ULID` + * :class:`~bytes` + * :class:`~bytearray` + + :param timestamp: Unix timestamp in seconds + :type timestamp: See docstring for types + :return: ULID using given timestamp and new randomness + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when the value is an unsupported type + :raises ValueError: when the value is a string and cannot be Base32 decoded + :raises ValueError: when the value is or was converted to something 48 bits + """ + timestamp = codec.decode_timestamp(timestamp) + randomness = self.provider.randomness(timestamp.bytes) + return self.create(timestamp, randomness) + + def from_randomness(self, randomness: codec.RandomnessPrimitive) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance using the given randomness value of a supported type. + + The following types are supported for randomness values: + + * :class:`~int` + * :class:`~float` + * :class:`~str` + * :class:`~memoryview` + * :class:`~ulid.ulid.Randomness` + * :class:`~ulid.ulid.ULID` + * :class:`~bytes` + * :class:`~bytearray` + + :param randomness: Random bytes + :type randomness: See docstring for types + :return: ULID using new timestamp and given randomness + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when the value is an unsupported type + :raises ValueError: when the value is a string and cannot be Base32 decoded + :raises ValueError: when the value is or was converted to something 80 bits + """ + timestamp = self.provider.timestamp() + return self.create(timestamp, randomness) + + @staticmethod + def create(timestamp: codec.TimestampPrimitive, randomness: codec.RandomnessPrimitive) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance using the given timestamp and randomness values. + + The following types are supported for timestamp values: + + * :class:`~datetime.datetime` + * :class:`~int` + * :class:`~float` + * :class:`~str` + * :class:`~memoryview` + * :class:`~ulid.ulid.Timestamp` + * :class:`~ulid.ulid.ULID` + * :class:`~bytes` + * :class:`~bytearray` + + The following types are supported for randomness values: + + * :class:`~int` + * :class:`~float` + * :class:`~str` + * :class:`~memoryview` + * :class:`~ulid.ulid.Randomness` + * :class:`~ulid.ulid.ULID` + * :class:`~bytes` + * :class:`~bytearray` + + :param timestamp: Unix timestamp in seconds + :type timestamp: See docstring for types + :param randomness: Random bytes + :type randomness: See docstring for types + :return: ULID using given timestamp and randomness + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when a value is an unsupported type + :raises ValueError: when a value is a string and cannot be Base32 decoded + :raises ValueError: when a value is or was converted to incorrect bit length + """ + timestamp = codec.decode_timestamp(timestamp) + randomness = codec.decode_randomness(randomness) + return ulid.ULID(timestamp.bytes + randomness.bytes) + + @staticmethod + def from_bytes(value: hints.Buffer) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~bytes`, + :class:`~bytearray`, or :class:`~memoryview` value. + + :param value: 16 bytes + :type value: :class:`~bytes`, :class:`~bytearray`, or :class:`~memoryview` + :return: ULID from buffer value + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when the value is not 16 bytes + """ + length = len(value) + if length != 16: + raise ValueError('Expects bytes to be 128 bits; got {} bytes'.format(length)) + + return ulid.ULID(value) + + @staticmethod + def from_int(value: int) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~int` value. + + :param value: 128 bit integer + :type value: :class:`~int` + :return: ULID from integer value + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when the value is not a 128 bit integer + """ + if value < 0: + raise ValueError('Expects positive integer') + + length = (value.bit_length() + 7) // 8 + if length > 16: + raise ValueError('Expects integer to be 128 bits; got {} bytes'.format(length)) + + return ulid.ULID(value.to_bytes(16, byteorder='big')) + + @staticmethod + def from_str(value: str) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` value. + + :param value: Base32 encoded string + :type value: :class:`~str` + :return: ULID from string value + :rtype: :class:`~ulid.ulid.ULID` + :raises ValueError: when the value is not 26 characters or malformed + """ + return ulid.ULID(base32.decode_ulid(value)) + + @staticmethod + def from_uuid(value: uuid.UUID) -> ulid.ULID: + """ + Create a new :class:`~ulid.ulid.ULID` instance from the given :class:`~uuid.UUID` value. + + :param value: UUIDv4 value + :type value: :class:`~uuid.UUID` + :return: ULID from UUID value + :rtype: :class:`~ulid.ulid.ULID` + """ + return ulid.ULID(value.bytes) diff --git a/ulid/api/default.py b/ulid/api/default.py new file mode 100644 index 0000000..8dbabe8 --- /dev/null +++ b/ulid/api/default.py @@ -0,0 +1,33 @@ +""" + ulid/api/default + ~~~~~~~~~~~~~~~~ + + Defaults the public API of the `ulid` package using the default provider. +""" +from .. import consts, providers, ulid +from . import api + +API = api.Api(providers.DEFAULT) + +create = API.create +from_bytes = API.from_bytes +from_int = API.from_int +from_randomness = API.from_randomness +from_str = API.from_str +from_timestamp = API.from_timestamp +from_uuid = API.from_uuid +new = API.new +parse = API.parse + +MIN_TIMESTAMP = consts.MIN_TIMESTAMP +MAX_TIMESTAMP = consts.MAX_TIMESTAMP +MIN_RANDOMNESS = consts.MIN_RANDOMNESS +MAX_RANDOMNESS = consts.MAX_RANDOMNESS +MIN_ULID = consts.MIN_ULID +MAX_ULID = consts.MAX_ULID + +Timestamp = ulid.Timestamp +Randomness = ulid.Randomness +ULID = ulid.ULID + +__all__ = api.ALL diff --git a/ulid/api/monotonic.py b/ulid/api/monotonic.py new file mode 100644 index 0000000..d4a17cc --- /dev/null +++ b/ulid/api/monotonic.py @@ -0,0 +1,33 @@ +""" + ulid/api/monotonic + ~~~~~~~~~~~~~~~~~~ + + Defaults the public API of the `ulid` package using a monotonic randomness provider. +""" +from .. import consts, providers, ulid +from . import api + +API = api.Api(providers.MONOTONIC) + +create = API.create +from_bytes = API.from_bytes +from_int = API.from_int +from_randomness = API.from_randomness +from_str = API.from_str +from_timestamp = API.from_timestamp +from_uuid = API.from_uuid +new = API.new +parse = API.parse + +MIN_TIMESTAMP = consts.MIN_TIMESTAMP +MAX_TIMESTAMP = consts.MAX_TIMESTAMP +MIN_RANDOMNESS = consts.MIN_RANDOMNESS +MAX_RANDOMNESS = consts.MAX_RANDOMNESS +MIN_ULID = consts.MIN_ULID +MAX_ULID = consts.MAX_ULID + +Timestamp = ulid.Timestamp +Randomness = ulid.Randomness +ULID = ulid.ULID + +__all__ = api.ALL diff --git a/ulid/consts.py b/ulid/consts.py new file mode 100644 index 0000000..f752cbf --- /dev/null +++ b/ulid/consts.py @@ -0,0 +1,32 @@ +""" + ulid/consts + ~~~~~~~~~~~ + + Contains public API constant values. +""" +from . import ulid + +__all__ = ['MIN_TIMESTAMP', 'MAX_TIMESTAMP', 'MIN_RANDOMNESS', 'MAX_RANDOMNESS', 'MIN_ULID', 'MAX_ULID'] + +#: Minimum possible timestamp value (0). +MIN_TIMESTAMP = ulid.Timestamp(b'\x00\x00\x00\x00\x00\x00') + + +#: Maximum possible timestamp value (281474976710.655 epoch). +MAX_TIMESTAMP = ulid.Timestamp(b'\xff\xff\xff\xff\xff\xff') + + +#: Minimum possible randomness value (0). +MIN_RANDOMNESS = ulid.Randomness(b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00') + + +#: Maximum possible randomness value (1208925819614629174706175). +MAX_RANDOMNESS = ulid.Randomness(b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff') + + +#: Minimum possible ULID value (0). +MIN_ULID = ulid.ULID(MIN_TIMESTAMP.bytes + MIN_RANDOMNESS.bytes) + + +#: Maximum possible ULID value (340282366920938463463374607431768211455). +MAX_ULID = ulid.ULID(MAX_TIMESTAMP.bytes + MAX_RANDOMNESS.bytes) diff --git a/ulid/hints.py b/ulid/hints.py index df32d39..ed8901b 100644 --- a/ulid/hints.py +++ b/ulid/hints.py @@ -5,6 +5,7 @@ Contains type hint definitions across modules in the package. """ import datetime +import types import typing import uuid @@ -24,12 +25,16 @@ Datetime = datetime.datetime # pylint: disable=invalid-name +#: Type hint that is an alias for the built-in :class:`~float` type. +Float = float # pylint: disable=invalid-name + + #: Type hint that is an alias for the built-in :class:`~int` type. Int = int # pylint: disable=invalid-name -#: Type hint that is an alias for the built-in :class:`~float` type. -Float = float # pylint: disable=invalid-name +#: Type hint that is an alias for the built-in :class:`~types.ModuleType` type. +Module = types.ModuleType #: Type hint that defines multiple primitive types that can represent parts or full ULID. diff --git a/ulid/providers/__init__.py b/ulid/providers/__init__.py new file mode 100644 index 0000000..33819ea --- /dev/null +++ b/ulid/providers/__init__.py @@ -0,0 +1,14 @@ +""" + ulid/providers + ~~~~~~~~~~~~~~ + + Contains functionality for timestamp/randomness data providers. +""" + +from . import base, default, monotonic + +Provider = base.Provider +DEFAULT = default.Provider() +MONOTONIC = monotonic.Provider(DEFAULT) + +__all__ = ['Provider', 'DEFAULT', 'MONOTONIC'] diff --git a/ulid/providers/base.py b/ulid/providers/base.py new file mode 100644 index 0000000..0ee31ef --- /dev/null +++ b/ulid/providers/base.py @@ -0,0 +1,35 @@ +""" + ulid/providers/provider + ~~~~~~~~~~~~~~~~~~~~~~~ + + Contains provider abstract classes. +""" +import abc + +from .. import hints + + +class Provider(metaclass=abc.ABCMeta): + """ + Abstract class that defines providers that yield timestamp and randomness values. + """ + + @abc.abstractmethod + def timestamp(self) -> hints.Bytes: + """ + Create a new timestamp value. + + :return: Timestamp value in bytes. + :rtype: :class:`~bytes` + """ + raise NotImplementedError('Method must be implemented by derived class') + + @abc.abstractmethod + def randomness(self, timestamp: hints.Bytes) -> hints.Bytes: + """ + Create a new randomness value. + + :return: Randomness value in bytes. + :rtype: :class:`~bytes` + """ + raise NotImplementedError('Method must be implemented by derived class') diff --git a/ulid/providers/default.py b/ulid/providers/default.py new file mode 100644 index 0000000..0d53922 --- /dev/null +++ b/ulid/providers/default.py @@ -0,0 +1,35 @@ +""" + ulid/providers/default + ~~~~~~~~~~~~~~~~~~~~~~ + + Contains data provider that creates new randomness values for the same timestamp. +""" +import os +import time + +from .. import hints +from . import base + + +class Provider(base.Provider): + """ + Provider that creates new randomness values for the same timestamp. + """ + + def timestamp(self) -> hints.Bytes: + """ + Create a new timestamp value. + + :return: Timestamp value in bytes. + :rtype: :class:`~bytes` + """ + return int(time.time() * 1000).to_bytes(6, byteorder='big') + + def randomness(self, timestamp: hints.Bytes) -> hints.Bytes: + """ + Create a new randomness value. + + :return: Randomness value in bytes. + :rtype: :class:`~bytes` + """ + return os.urandom(10) diff --git a/ulid/providers/monotonic.py b/ulid/providers/monotonic.py new file mode 100644 index 0000000..8fe304e --- /dev/null +++ b/ulid/providers/monotonic.py @@ -0,0 +1,57 @@ +""" + ulid/providers/monotonic + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Contains data provider that monotonically increases randomness values for the same timestamp. +""" +import threading + +from .. import consts, hints, ulid +from . import base + + +class Provider(base.Provider): + """ + Provider that monotonically increases randomness values for the same timestamp. + """ + def __init__(self, default: base.Provider): + self.default = default + self.lock = threading.Lock() + self.prev_timestamp = consts.MIN_TIMESTAMP + self.prev_randomness = consts.MIN_RANDOMNESS + + def timestamp(self) -> hints.Bytes: + """ + Create a new timestamp value. + + :return: Timestamp value in bytes. + :rtype: :class:`~bytes` + """ + return self.default.timestamp() + + def randomness(self, timestamp: hints.Bytes) -> hints.Bytes: + """ + Create a new randomness value. + + :param timestamp: Timestamp value in bytes + :type timestamp: :class:`~bytes` + :return: Randomness value in bytes. + :rtype: :class:`~bytes` + """ + with self.lock: + ts = ulid.Timestamp(timestamp) + + # Randomness requested for new timestamp. + if ts > self.prev_timestamp: + self.prev_randomness = ulid.Randomness(self.default.randomness(ts.bytes)) + self.prev_timestamp = ts + + # Randomness requested for same timestamp as previous request. + else: + if self.prev_randomness == consts.MAX_RANDOMNESS: + raise ValueError('Monotonic randomness value too large and will overflow') + + next_value = (self.prev_randomness.int + 1).to_bytes(10, byteorder='big') + self.prev_randomness = ulid.Randomness(next_value) + + return self.prev_randomness.bytes