diff --git a/.pylintrc b/.pylintrc index da003fb..5e71226 100644 --- a/.pylintrc +++ b/.pylintrc @@ -65,7 +65,7 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=filter-builtin-not-iterating,raw_input-builtin,cmp-builtin,buffer-builtin,unpacking-in-except,xrange-builtin,old-ne-operator,backtick,coerce-method,standarderror-builtin,long-suffix,reload-builtin,file-builtin,round-builtin,intern-builtin,raising-string,zip-builtin-not-iterating,no-absolute-import,unichr-builtin,parameter-unpacking,input-builtin,print-statement,indexing-exception,delslice-method,setslice-method,nonzero-method,long-builtin,hex-method,basestring-builtin,next-method-called,import-star-module-level,coerce-builtin,old-division,oct-method,map-builtin-not-iterating,range-builtin-not-iterating,reduce-builtin,apply-builtin,dict-view-method,useless-suppression,old-octal-literal,execfile-builtin,dict-iter-method,unicode-builtin,using-cmp-argument,metaclass-assignment,cmp-method,getslice-method,old-raise-syntax,suppressed-message +disable=filter-builtin-not-iterating,raw_input-builtin,cmp-builtin,buffer-builtin,unpacking-in-except,xrange-builtin,old-ne-operator,backtick,coerce-method,standarderror-builtin,long-suffix,reload-builtin,file-builtin,round-builtin,intern-builtin,raising-string,zip-builtin-not-iterating,no-absolute-import,unichr-builtin,parameter-unpacking,input-builtin,print-statement,indexing-exception,delslice-method,setslice-method,nonzero-method,long-builtin,hex-method,basestring-builtin,next-method-called,import-star-module-level,coerce-builtin,old-division,oct-method,map-builtin-not-iterating,range-builtin-not-iterating,reduce-builtin,apply-builtin,dict-view-method,useless-suppression,old-octal-literal,execfile-builtin,dict-iter-method,unicode-builtin,using-cmp-argument,metaclass-assignment,cmp-method,getslice-method,old-raise-syntax,suppressed-message,duplicate-code [REPORTS] diff --git a/tests/test_api_api.py b/tests/test_api_api.py new file mode 100644 index 0000000..f841d13 --- /dev/null +++ b/tests/test_api_api.py @@ -0,0 +1,93 @@ +""" + test_api_api + ~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.api.api` module. +""" +import pytest + +from ulid import providers +from ulid.api.api import ALL, Api + + +@pytest.fixture(scope='function') +def mock_provider(mocker): + """ + Fixture that yields a mock provider. + """ + provider = mocker.Mock(spec=providers.Provider) + provider.timestamp = mocker.Mock(side_effect=providers.DEFAULT.timestamp) + provider.randomness = mocker.Mock(side_effect=providers.DEFAULT.randomness) + return provider + + +@pytest.fixture(scope='function') +def mock_api(mock_provider): + """ + Fixture that yields a :class:`~ulid.api.api.Api` instance with a mock provider. + """ + return Api(mock_provider) + + +def test_all_defined_expected_methods(): + """ + Assert that :attr:`~ulid.api.api.ALL` exposes expected interface. + """ + assert 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' + ] + + +def test_api_new_calls_provider_timestamp(mock_api): + """ + Assert :meth:`~ulid.api.api.Api.new` calls :meth:`~ulid.providers.base.Provider.timestamp` for a value. + """ + mock_api.new() + + mock_api.provider.timestamp.assert_called_once_with() + + +def test_api_new_calls_provider_randomness(mocker, mock_api): + """ + Assert :meth:`~ulid.api.api.Api.new` calls :meth:`~ulid.providers.base.Provider.randomness` for a value. + """ + mock_api.new() + + mock_api.provider.randomness.assert_called_once_with(mocker.ANY) + + +def test_api_from_timestamp_calls_provider_randomness(mocker, mock_api, valid_bytes_48): + """ + Assert :meth:`~ulid.api.api.Api.from_timestamp` calls :meth:`~ulid.providers.base.Provider.randomness` for a value. + """ + mock_api.from_timestamp(valid_bytes_48) + + mock_api.provider.timestamp.assert_not_called() + mock_api.provider.randomness.assert_called_once_with(mocker.ANY) + + +def test_api_from_randomness_calls_provider_timestamp(mock_api, valid_bytes_80): + """ + Assert :meth:`~ulid.api.api.Api.from_randomness` calls :meth:`~ulid.providers.base.Provider.timestamp` for a value. + """ + mock_api.from_randomness(valid_bytes_80) + + mock_api.provider.timestamp.assert_called_once_with() + mock_api.provider.randomness.assert_not_called() diff --git a/tests/test_api_default.py b/tests/test_api_default.py new file mode 100644 index 0000000..6e0edbc --- /dev/null +++ b/tests/test_api_default.py @@ -0,0 +1,23 @@ +""" + test_api_default + ~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.api.default` module. +""" +from ulid.api import default +from ulid.api.api import ALL + + +def test_module_has_dunder_all(): + """ + Assert that :mod:`~ulid.api.default` exposes the :attr:`~ulid.api.__all__` attribute as a list. + """ + assert hasattr(default, '__all__') + assert isinstance(default.__all__, list) + + +def test_module_exposes_expected_interface(): + """ + Assert that :attr:`~ulid.api.default.__all__` exposes expected interface. + """ + assert default.__all__ == ALL diff --git a/tests/test_api_monotonic.py b/tests/test_api_monotonic.py new file mode 100644 index 0000000..68d3695 --- /dev/null +++ b/tests/test_api_monotonic.py @@ -0,0 +1,23 @@ +""" + test_api_monotonic + ~~~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.api.monotonic` module. +""" +from ulid.api import monotonic +from ulid.api.api import ALL + + +def test_module_has_dunder_all(): + """ + Assert that :mod:`~ulid.api.monotonic` exposes the :attr:`~ulid.api.__all__` attribute as a list. + """ + assert hasattr(monotonic, '__all__') + assert isinstance(monotonic.__all__, list) + + +def test_module_exposes_expected_interface(): + """ + Assert that :attr:`~ulid.api.monotonic.__all__` exposes expected interface. + """ + assert monotonic.__all__ == ALL diff --git a/tests/test_api.py b/tests/test_api_package.py similarity index 78% rename from tests/test_api.py rename to tests/test_api_package.py index 16801fe..8adb122 100644 --- a/tests/test_api.py +++ b/tests/test_api_package.py @@ -1,8 +1,8 @@ """ - test_api - ~~~~~~~~ + test_api_package + ~~~~~~~~~~~~~~~~ - Tests for the :mod:`~ulid.api` module. + Tests for the :pkg:`~ulid.api` package interface. """ import datetime import time @@ -10,7 +10,9 @@ import pytest -from ulid import api, base32, ulid +from ulid import base32, consts, ulid +from ulid.api import default, monotonic +from ulid.api.api import ALL BYTES_SIZE_EXC_REGEX = r'Expects bytes to be 128 bits' INT_SIZE_EXC_REGEX = r'Expects integer to be 128 bits' @@ -26,7 +28,18 @@ PARSE_UNSUPPORTED_TYPE_REGEX = r'^Cannot create ULID from type' -@pytest.fixture(scope='session', params=[ +@pytest.fixture(scope='module', params=[ + default, + monotonic +]) +def api(request): + """ + Fixture that yields a :class:`~ulid.api.api.Api` instance. + """ + return request.param + + +@pytest.fixture(scope='module', params=[ list, dict, set, @@ -40,7 +53,7 @@ def unsupported_type(request): return request.param -@pytest.fixture(scope='session', params=[bytes, bytearray, memoryview]) +@pytest.fixture(scope='module', params=[bytes, bytearray, memoryview]) def buffer_type(request): """ Fixture that yields types that support the buffer protocol. @@ -48,62 +61,77 @@ def buffer_type(request): return request.param -def test_min_timestamp_uses_expected_value(): +def test_package_has_dunder_all(api): + """ + Assert that :pkg:`~ulid.api` exposes the :attr:`~ulid.api.__all__` attribute as a list. + """ + assert hasattr(api, '__all__') + assert isinstance(api.__all__, list) + + +def test_package_exposes_expected_interface(api): + """ + Assert that :attr:`~ulid.providers.__all__` exposes expected interface. + """ + assert api.__all__ == ALL + + +def test_min_timestamp_uses_expected_value(api): """ Assert that :func:`~ulid.api.MIN_TIMESTAMP` uses expected byte value. """ value = api.MIN_TIMESTAMP - assert value == b'\x00\x00\x00\x00\x00\x00' + assert value == consts.MIN_TIMESTAMP -def test_max_timestamp_uses_expected_value(): +def test_max_timestamp_uses_expected_value(api): """ Assert that :func:`~ulid.api.MAX_RANDOMNESS` uses expected byte value. """ value = api.MAX_TIMESTAMP - assert value == b'\xff\xff\xff\xff\xff\xff' + assert value == consts.MAX_TIMESTAMP -def test_min_randomness_uses_expected_value(): +def test_min_randomness_uses_expected_value(api): """ Assert that :func:`~ulid.api.MIN_RANDOMNESS` uses expected byte value. """ value = api.MIN_RANDOMNESS - assert value == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + assert value == consts.MIN_RANDOMNESS -def test_max_randomness_uses_expected_value(): +def test_max_randomness_uses_expected_value(api): """ Assert that :func:`~ulid.api.MAX_RANDOMNESS` uses expected byte value. """ value = api.MAX_RANDOMNESS - assert value == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + assert value == consts.MAX_RANDOMNESS -def test_min_ulid_uses_expected_value(): +def test_min_ulid_uses_expected_value(api): """ Assert that :func:`~ulid.api.MIN_ULID` uses expected byte value. """ value = api.MIN_ULID - assert value == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + assert value == consts.MIN_ULID -def test_max_ulid_uses_expected_value(): +def test_max_ulid_uses_expected_value(api): """ Assert that :func:`~ulid.api.MAX_ULID` uses expected byte value. """ value = api.MAX_ULID - assert value == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + assert value == consts.MAX_ULID -def test_new_returns_ulid_instance(): +def test_new_returns_ulid_instance(api): """ Assert that :func:`~ulid.api.new` returns a new :class:`~ulid.ulid.ULID` instance. """ assert isinstance(api.new(), ulid.ULID) -def test_parse_returns_given_ulid_instance(): +def test_parse_returns_given_ulid_instance(api): """ Assert that :func:`~ulid.api.parse` returns the given :class:`~ulid.ulid.ULID` instance when given one. @@ -114,7 +142,7 @@ def test_parse_returns_given_ulid_instance(): assert instance == value -def test_parse_returns_ulid_instance_from_uuid(): +def test_parse_returns_ulid_instance_from_uuid(api): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~uuid.UUID`. @@ -125,7 +153,7 @@ def test_parse_returns_ulid_instance_from_uuid(): assert instance.bytes == value.bytes -def test_parse_returns_ulid_instance_from_uuid_str(): +def test_parse_returns_ulid_instance_from_uuid_str(api): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~uuid.UUID` instance in its string format. @@ -136,7 +164,7 @@ def test_parse_returns_ulid_instance_from_uuid_str(): assert instance.bytes == value.bytes -def test_parse_returns_ulid_instance_from_uuid_hex_str(): +def test_parse_returns_ulid_instance_from_uuid_hex_str(api): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~uuid.UUID` instance in its hex string format. @@ -147,7 +175,7 @@ def test_parse_returns_ulid_instance_from_uuid_hex_str(): assert instance.bytes == value.bytes -def test_parse_returns_ulid_instance_from_ulid_str(valid_bytes_128): +def test_parse_returns_ulid_instance_from_ulid_str(api, valid_bytes_128): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` instance that represents a fill ULID. @@ -158,7 +186,7 @@ def test_parse_returns_ulid_instance_from_ulid_str(valid_bytes_128): assert instance.bytes == valid_bytes_128 -def test_parse_returns_ulid_instance_from_randomness_str(valid_bytes_80): +def test_parse_returns_ulid_instance_from_randomness_str(api, valid_bytes_80): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` instance that represents randomness data. @@ -169,7 +197,7 @@ def test_parse_returns_ulid_instance_from_randomness_str(valid_bytes_80): assert instance.randomness().str == value -def test_parse_returns_ulid_instance_from_timestamp_str(valid_bytes_48): +def test_parse_returns_ulid_instance_from_timestamp_str(api, valid_bytes_48): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` instance that represents timestamp data. @@ -180,7 +208,7 @@ def test_parse_returns_ulid_instance_from_timestamp_str(valid_bytes_48): assert instance.timestamp().str == value -def test_parse_error_on_invalid_length_str(invalid_str_10_16_26_32_36): +def test_parse_error_on_invalid_length_str(api, invalid_str_10_16_26_32_36): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from the given :class:`~str` instance that represents timestamp data. @@ -190,7 +218,7 @@ def test_parse_error_on_invalid_length_str(invalid_str_10_16_26_32_36): assert ex.match(PARSE_STR_LEN_EXC_REGEX) -def test_parse_returns_ulid_instance_from_int(valid_bytes_128): +def test_parse_returns_ulid_instance_from_int(api, valid_bytes_128): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from a valid ULID stored as an int. @@ -201,7 +229,7 @@ def test_parse_returns_ulid_instance_from_int(valid_bytes_128): assert instance.bytes == valid_bytes_128 -def test_parse_raises_when_int_greater_than_128_bits(invalid_bytes_128_overflow): +def test_parse_raises_when_int_greater_than_128_bits(api, invalid_bytes_128_overflow): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when given int cannot be stored in 128 bits. @@ -212,7 +240,7 @@ def test_parse_raises_when_int_greater_than_128_bits(invalid_bytes_128_overflow) assert ex.match(INT_SIZE_EXC_REGEX) -def test_parse_raises_when_int_negative(): +def test_parse_raises_when_int_negative(api): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when given a negative int number. @@ -222,7 +250,7 @@ def test_parse_raises_when_int_negative(): assert ex.match(INT_NEGATIVE_EXC_REGEX) -def test_parse_returns_ulid_instance_from_float(valid_bytes_128): +def test_parse_returns_ulid_instance_from_float(api, valid_bytes_128): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from a valid ULID stored as a float. @@ -233,7 +261,7 @@ def test_parse_returns_ulid_instance_from_float(valid_bytes_128): assert instance.int == int(value) -def test_parse_raises_when_float_greater_than_128_bits(invalid_bytes_128_overflow): +def test_parse_raises_when_float_greater_than_128_bits(api, invalid_bytes_128_overflow): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when given float cannot be stored in 128 bits. @@ -244,7 +272,7 @@ def test_parse_raises_when_float_greater_than_128_bits(invalid_bytes_128_overflo assert ex.match(INT_SIZE_EXC_REGEX) -def test_parse_raises_when_float_negative(): +def test_parse_raises_when_float_negative(api): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when given a negative float number. @@ -254,7 +282,7 @@ def test_parse_raises_when_float_negative(): assert ex.match(INT_NEGATIVE_EXC_REGEX) -def test_parse_returns_ulid_instance_from_buffer_type(buffer_type, valid_bytes_128): +def test_parse_returns_ulid_instance_from_buffer_type(api, buffer_type, valid_bytes_128): """ Assert that :func:`~ulid.api.parse` returns a new :class:`~ulid.ulid.ULID` instance from a valid set of 128 bytes representing by the given buffer type. @@ -265,7 +293,7 @@ def test_parse_returns_ulid_instance_from_buffer_type(buffer_type, valid_bytes_1 assert instance.bytes == valid_bytes_128 -def test_parse_raises_when_buffer_type_not_128_bits(buffer_type, invalid_bytes_128): +def test_parse_raises_when_buffer_type_not_128_bits(api, buffer_type, invalid_bytes_128): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when given bytes that is not 128 bit in length. @@ -276,7 +304,7 @@ def test_parse_raises_when_buffer_type_not_128_bits(buffer_type, invalid_bytes_1 assert ex.match(BYTES_SIZE_EXC_REGEX) -def test_parse_raises_when_given_unsupported_type(unsupported_type): +def test_parse_raises_when_given_unsupported_type(api, unsupported_type): """ Assert that :func:`~ulid.api.parse` raises a :class:`~ValueError` when a value of an unsupported type. @@ -286,7 +314,7 @@ def test_parse_raises_when_given_unsupported_type(unsupported_type): assert ex.match(PARSE_UNSUPPORTED_TYPE_REGEX) -def test_create_timestamp_datetime_returns_ulid_instance(valid_bytes_80): +def test_create_timestamp_datetime_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as an :class:`~datetime.datetime`. @@ -297,7 +325,7 @@ def test_create_timestamp_datetime_returns_ulid_instance(valid_bytes_80): assert int(instance.timestamp().timestamp) == int(value.timestamp()) -def test_create_timestamp_int_returns_ulid_instance(valid_bytes_80): +def test_create_timestamp_int_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as an :class:`~int`. @@ -308,7 +336,7 @@ def test_create_timestamp_int_returns_ulid_instance(valid_bytes_80): assert int(instance.timestamp().timestamp) == value -def test_create_timestamp_float_returns_ulid_instance(valid_bytes_80): +def test_create_timestamp_float_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as a :class:`~float`. @@ -319,7 +347,7 @@ def test_create_timestamp_float_returns_ulid_instance(valid_bytes_80): assert int(instance.timestamp().timestamp) == int(value) -def test_create_timestamp_str_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_timestamp_str_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~str`. @@ -330,7 +358,7 @@ def test_create_timestamp_str_returns_ulid_instance(valid_bytes_48, valid_bytes_ assert instance.timestamp().str == value -def test_create_timestamp_bytes_returns_ulid_instance(buffer_type, valid_bytes_48, valid_bytes_80): +def test_create_timestamp_bytes_returns_ulid_instance(api, buffer_type, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as an object that supports the buffer protocol. @@ -341,7 +369,7 @@ def test_create_timestamp_bytes_returns_ulid_instance(buffer_type, valid_bytes_4 assert instance.timestamp().bytes == value -def test_create_timestamp_timestamp_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_timestamp_timestamp_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~ulid.ulid.Timestamp`. @@ -352,7 +380,7 @@ def test_create_timestamp_timestamp_returns_ulid_instance(valid_bytes_48, valid_ assert instance.timestamp() == value -def test_create_timestamp_ulid_returns_ulid_instance(valid_bytes_128, valid_bytes_80): +def test_create_timestamp_ulid_returns_ulid_instance(api, valid_bytes_128, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~ulid.ulid.ULID`. @@ -363,7 +391,7 @@ def test_create_timestamp_ulid_returns_ulid_instance(valid_bytes_128, valid_byte assert instance.timestamp() == value.timestamp() -def test_create_raises_when_given_unsupported_timestamp_type(unsupported_type, valid_bytes_80): +def test_create_raises_when_given_unsupported_timestamp_type(api, unsupported_type, valid_bytes_80): """ Assert that :func:`~ulid.api.create` raises a :class:`~ValueError` when timestamp value of an unsupported type. @@ -373,7 +401,7 @@ def test_create_raises_when_given_unsupported_timestamp_type(unsupported_type, v assert ex.match(UNSUPPORTED_TIMESTAMP_TYPE_EXC_REGEX) -def test_create_randomness_int_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_randomness_int_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an :class:`~int`. @@ -384,7 +412,7 @@ def test_create_randomness_int_returns_ulid_instance(valid_bytes_48, valid_bytes assert instance.randomness().int == value -def test_create_randomness_float_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_randomness_float_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an :class:`~float`. @@ -395,7 +423,7 @@ def test_create_randomness_float_returns_ulid_instance(valid_bytes_48, valid_byt assert instance.randomness().int == int(value) -def test_create_randomness_str_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_randomness_str_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance @@ -407,7 +435,7 @@ def test_create_randomness_str_returns_ulid_instance(valid_bytes_48, valid_bytes assert instance.randomness().str == value -def test_create_randomness_bytes_returns_ulid_instance(buffer_type, valid_bytes_48, valid_bytes_80): +def test_create_randomness_bytes_returns_ulid_instance(api, buffer_type, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an object that supports the buffer protocol. @@ -418,7 +446,7 @@ def test_create_randomness_bytes_returns_ulid_instance(buffer_type, valid_bytes_ assert instance.randomness().bytes == value -def test_create_randomness_randomness_returns_ulid_instance(valid_bytes_48, valid_bytes_80): +def test_create_randomness_randomness_returns_ulid_instance(api, valid_bytes_48, valid_bytes_80): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as a :class:`~ulid.ulid.Randomness`. @@ -429,7 +457,7 @@ def test_create_randomness_randomness_returns_ulid_instance(valid_bytes_48, vali assert instance.randomness() == value -def test_create_randomness_ulid_returns_ulid_instance(valid_bytes_48, valid_bytes_128): +def test_create_randomness_ulid_returns_ulid_instance(api, valid_bytes_48, valid_bytes_128): """ Assert that :func:`~ulid.api.create` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as a :class:`~ulid.ulid.ULID`. @@ -440,7 +468,7 @@ def test_create_randomness_ulid_returns_ulid_instance(valid_bytes_48, valid_byte assert instance.randomness() == value.randomness() -def test_create_raises_when_given_unsupported_randomness_type(unsupported_type, valid_bytes_48): +def test_create_raises_when_given_unsupported_randomness_type(api, unsupported_type, valid_bytes_48): """ Assert that :func:`~ulid.api.create` raises a :class:`~ValueError` when randomness value of an unsupported type. @@ -450,7 +478,7 @@ def test_create_raises_when_given_unsupported_randomness_type(unsupported_type, assert ex.match(UNSUPPORTED_RANDOMNESS_TYPE_EXC_REGEX) -def test_from_bytes_returns_ulid_instance(buffer_type, valid_bytes_128): +def test_from_bytes_returns_ulid_instance(api, buffer_type, valid_bytes_128): """ Assert that :func:`~ulid.api.from_bytes` returns a new :class:`~ulid.ulid.ULID` instance from the given bytes. @@ -461,7 +489,7 @@ def test_from_bytes_returns_ulid_instance(buffer_type, valid_bytes_128): assert instance.bytes == valid_bytes_128 -def test_from_bytes_raises_when_not_128_bits(buffer_type, invalid_bytes_128): +def test_from_bytes_raises_when_not_128_bits(api, buffer_type, invalid_bytes_128): """ Assert that :func:`~ulid.api.from_bytes` raises a :class:`~ValueError` when given bytes that is not 128 bit in length. @@ -472,7 +500,7 @@ def test_from_bytes_raises_when_not_128_bits(buffer_type, invalid_bytes_128): assert ex.match(BYTES_SIZE_EXC_REGEX) -def test_from_int_returns_ulid_instance(valid_bytes_128): +def test_from_int_returns_ulid_instance(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_int` returns a new :class:`~ulid.ulid.ULID` instance from the given bytes. @@ -483,7 +511,7 @@ def test_from_int_returns_ulid_instance(valid_bytes_128): assert instance.bytes == valid_bytes_128 -def test_from_int_raises_when_greater_than_128_bits(invalid_bytes_128_overflow): +def test_from_int_raises_when_greater_than_128_bits(api, invalid_bytes_128_overflow): """ Assert that :func:`~ulid.api.from_int` raises a :class:`~ValueError` when given int cannot be stored in 128 bits. @@ -494,7 +522,7 @@ def test_from_int_raises_when_greater_than_128_bits(invalid_bytes_128_overflow): assert ex.match(INT_SIZE_EXC_REGEX) -def test_from_int_raises_when_negative_number(): +def test_from_int_raises_when_negative_number(api): """ Assert that :func:`~ulid.api.from_int` raises a :class:`~ValueError` when given a negative number. @@ -504,7 +532,7 @@ def test_from_int_raises_when_negative_number(): assert ex.match(INT_NEGATIVE_EXC_REGEX) -def test_from_str_returns_ulid_instance(valid_bytes_128): +def test_from_str_returns_ulid_instance(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_str` returns a new :class:`~ulid.ulid.ULID` instance from the given bytes. @@ -515,7 +543,7 @@ def test_from_str_returns_ulid_instance(valid_bytes_128): assert instance.bytes == valid_bytes_128 -def test_from_str_raises_when_not_128_bits(valid_bytes_48): +def test_from_str_raises_when_not_128_bits(api, valid_bytes_48): """ Assert that :func:`~ulid.api.from_str` raises a :class:`~ValueError` when given bytes that is not 128 bit in length. @@ -526,7 +554,7 @@ def test_from_str_raises_when_not_128_bits(valid_bytes_48): assert ex.match(STR_SIZE_EXC_REGEX) -def test_from_uuid_returns_ulid_instance(): +def test_from_uuid_returns_ulid_instance(api): """ Assert that :func:`~ulid.api.from_uuid` returns a new :class:`~ulid.ulid.ULID` instance from the underlying bytes of the UUID. @@ -537,7 +565,7 @@ def test_from_uuid_returns_ulid_instance(): assert instance.bytes == value.bytes -def test_from_timestamp_datetime_returns_ulid_instance(): +def test_from_timestamp_datetime_returns_ulid_instance(api): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as an :class:`~datetime.datetime`. @@ -548,7 +576,7 @@ def test_from_timestamp_datetime_returns_ulid_instance(): assert int(instance.timestamp().timestamp) == int(value.timestamp()) -def test_from_timestamp_int_returns_ulid_instance(): +def test_from_timestamp_int_returns_ulid_instance(api): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as an :class:`~int`. @@ -559,7 +587,7 @@ def test_from_timestamp_int_returns_ulid_instance(): assert int(instance.timestamp().timestamp) == value -def test_from_timestamp_float_returns_ulid_instance(): +def test_from_timestamp_float_returns_ulid_instance(api): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given Unix time from epoch in seconds as a :class:`~float`. @@ -570,7 +598,7 @@ def test_from_timestamp_float_returns_ulid_instance(): assert int(instance.timestamp().timestamp) == int(value) -def test_from_timestamp_str_returns_ulid_instance(valid_bytes_48): +def test_from_timestamp_str_returns_ulid_instance(api, valid_bytes_48): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~str`. @@ -581,7 +609,7 @@ def test_from_timestamp_str_returns_ulid_instance(valid_bytes_48): assert instance.timestamp().str == value -def test_from_timestamp_bytes_returns_ulid_instance(buffer_type, valid_bytes_48): +def test_from_timestamp_bytes_returns_ulid_instance(api, buffer_type, valid_bytes_48): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as an object that supports the buffer protocol. @@ -592,7 +620,7 @@ def test_from_timestamp_bytes_returns_ulid_instance(buffer_type, valid_bytes_48) assert instance.timestamp().bytes == value -def test_from_timestamp_timestamp_returns_ulid_instance(valid_bytes_48): +def test_from_timestamp_timestamp_returns_ulid_instance(api, valid_bytes_48): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~ulid.ulid.Timestamp`. @@ -603,7 +631,7 @@ def test_from_timestamp_timestamp_returns_ulid_instance(valid_bytes_48): assert instance.timestamp() == value -def test_from_timestamp_ulid_returns_ulid_instance(valid_bytes_128): +def test_from_timestamp_ulid_returns_ulid_instance(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_timestamp` returns a new :class:`~ulid.ulid.ULID` instance from the given timestamp as a :class:`~ulid.ulid.ULID`. @@ -614,7 +642,7 @@ def test_from_timestamp_ulid_returns_ulid_instance(valid_bytes_128): assert instance.timestamp() == value.timestamp() -def test_from_timestamp_with_unsupported_type_raises(unsupported_type): +def test_from_timestamp_with_unsupported_type_raises(api, unsupported_type): """ Assert that :func:`~ulid.api.from_timestamp` raises a :class:`~ValueError` when given a type it cannot compute a timestamp value from. @@ -624,7 +652,7 @@ def test_from_timestamp_with_unsupported_type_raises(unsupported_type): assert ex.match(UNSUPPORTED_TIMESTAMP_TYPE_EXC_REGEX) -def test_from_timestamp_with_incorrect_size_bytes_raises(valid_bytes_128): +def test_from_timestamp_with_incorrect_size_bytes_raises(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_timestamp` raises a :class:`~ValueError` when given a type that cannot be represented as exactly 48 bits. @@ -634,7 +662,7 @@ def test_from_timestamp_with_incorrect_size_bytes_raises(valid_bytes_128): assert ex.match(TIMESTAMP_SIZE_EXC_REGEX) -def test_from_randomness_int_returns_ulid_instance(valid_bytes_80): +def test_from_randomness_int_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an :class:`~int`. @@ -645,7 +673,7 @@ def test_from_randomness_int_returns_ulid_instance(valid_bytes_80): assert instance.randomness().int == value -def test_from_randomness_float_returns_ulid_instance(valid_bytes_80): +def test_from_randomness_float_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an :class:`~float`. @@ -656,7 +684,7 @@ def test_from_randomness_float_returns_ulid_instance(valid_bytes_80): assert instance.randomness().int == int(value) -def test_from_randomness_str_returns_ulid_instance(valid_bytes_80): +def test_from_randomness_str_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance @@ -668,7 +696,7 @@ def test_from_randomness_str_returns_ulid_instance(valid_bytes_80): assert instance.randomness().str == value -def test_from_randomness_bytes_returns_ulid_instance(buffer_type, valid_bytes_80): +def test_from_randomness_bytes_returns_ulid_instance(api, buffer_type, valid_bytes_80): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as an object that supports the buffer protocol. @@ -679,7 +707,7 @@ def test_from_randomness_bytes_returns_ulid_instance(buffer_type, valid_bytes_80 assert instance.randomness().bytes == value -def test_from_randomness_randomness_returns_ulid_instance(valid_bytes_80): +def test_from_randomness_randomness_returns_ulid_instance(api, valid_bytes_80): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as a :class:`~ulid.ulid.Randomness`. @@ -690,7 +718,7 @@ def test_from_randomness_randomness_returns_ulid_instance(valid_bytes_80): assert instance.randomness() == value -def test_from_randomness_ulid_returns_ulid_instance(valid_bytes_128): +def test_from_randomness_ulid_returns_ulid_instance(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_randomness` returns a new :class:`~ulid.ulid.ULID` instance from the given random values as a :class:`~ulid.ulid.ULID`. @@ -701,7 +729,7 @@ def test_from_randomness_ulid_returns_ulid_instance(valid_bytes_128): assert instance.randomness() == value.randomness() -def test_from_randomness_with_unsupported_type_raises(unsupported_type): +def test_from_randomness_with_unsupported_type_raises(api, unsupported_type): """ Assert that :func:`~ulid.api.from_randomness` raises a :class:`~ValueError` when given a type it cannot compute a randomness value from. @@ -711,7 +739,7 @@ def test_from_randomness_with_unsupported_type_raises(unsupported_type): assert ex.match(UNSUPPORTED_RANDOMNESS_TYPE_EXC_REGEX) -def test_from_randomness_with_incorrect_size_bytes_raises(valid_bytes_128): +def test_from_randomness_with_incorrect_size_bytes_raises(api, valid_bytes_128): """ Assert that :func:`~ulid.api.from_randomness` raises a :class:`~ValueError` when given a type that cannot be represented as exactly 80 bits. diff --git a/tests/test_consts.py b/tests/test_consts.py new file mode 100644 index 0000000..4b6e1bf --- /dev/null +++ b/tests/test_consts.py @@ -0,0 +1,57 @@ +""" + test_module + ~~~~~~~~~~~ + + Tests for the :mod:`~ulid` module. +""" +import pytest + +from ulid import consts + + +def test_min_timestamp_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MIN_TIMESTAMP` uses expected byte value. + """ + value = consts.MIN_TIMESTAMP + assert value == b'\x00\x00\x00\x00\x00\x00' + + +def test_max_timestamp_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MAX_RANDOMNESS` uses expected byte value. + """ + value = consts.MAX_TIMESTAMP + assert value == b'\xff\xff\xff\xff\xff\xff' + + +def test_min_randomness_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MIN_RANDOMNESS` uses expected byte value. + """ + value = consts.MIN_RANDOMNESS + assert value == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + +def test_max_randomness_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MAX_RANDOMNESS` uses expected byte value. + """ + value = consts.MAX_RANDOMNESS + assert value == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' + + +def test_min_ulid_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MIN_ULID` uses expected byte value. + """ + value = consts.MIN_ULID + assert value == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + + +def test_max_ulid_uses_expected_value(): + """ + Assert that :func:`~ulid.consts.MAX_ULID` uses expected byte value. + """ + value = consts.MAX_ULID + assert value == b'\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff' 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/tests/test_providers_base.py b/tests/test_providers_base.py new file mode 100644 index 0000000..86aa2c6 --- /dev/null +++ b/tests/test_providers_base.py @@ -0,0 +1,16 @@ +""" + test_providers_base + ~~~~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.providers.base` module. +""" +import inspect + +from ulid.providers import base + + +def test_provider_is_abstract(): + """ + Assert that :class:`~ulid.providers.base.Provider` is an abstract class. + """ + assert inspect.isabstract(base.Provider) diff --git a/tests/test_providers_default.py b/tests/test_providers_default.py new file mode 100644 index 0000000..2546a21 --- /dev/null +++ b/tests/test_providers_default.py @@ -0,0 +1,71 @@ +""" + test_providers_default + ~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.providers.default` module. +""" +import time + +import pytest + +from ulid.providers import base, default + + +@pytest.fixture(scope='function') +def provider(): + """ + Fixture that yields a default provider instance. + """ + return default.Provider() + + +def test_provider_derives_from_base(): + """ + Assert that :class:`~ulid.providers.default.Provider` derives from :class:`~ulid.providers.base.Provider`. + """ + assert issubclass(default.Provider, base.Provider) + + +def test_provider_timestamp_returns_bytes(provider): + """ + Assert that :meth:`~ulid.providers.default.Provider.timestamp` returns bytes of expected length. + """ + value = provider.timestamp() + assert isinstance(value, bytes) + assert len(value) == 6 + + +def test_provider_timestamp_uses_time_epoch(provider): + """ + Assert that :meth:`~ulid.providers.default.Provider.timestamp` returns the current time milliseconds + since epoch in bytes. + """ + timestamp_bytes = provider.timestamp() + timestamp_int = int.from_bytes(timestamp_bytes, byteorder='big') + assert timestamp_int // 1000 < time.time() + + +def test_provider_randomness_returns_bytes(provider): + """ + Assert that :meth:`~ulid.providers.default.Provider.randomness` returns bytes of expected length. + """ + value = provider.randomness(provider.timestamp()) + assert isinstance(value, bytes) + assert len(value) == 10 + + +def test_provider_randomness_returns_random_values_for_same_timestamp(provider): + """ + Assert that :meth:`~ulid.providers.default.Provider.randomness` returns random bytes + when given the same timestamp. + """ + timestamp = provider.timestamp() + x = provider.randomness(timestamp) + y = provider.randomness(timestamp) + + assert x != y + + x_int = int.from_bytes(x, byteorder='big') + y_int = int.from_bytes(y, byteorder='big') + + assert x_int + 1 != y_int diff --git a/tests/test_providers_monotonic.py b/tests/test_providers_monotonic.py new file mode 100644 index 0000000..474e5e9 --- /dev/null +++ b/tests/test_providers_monotonic.py @@ -0,0 +1,86 @@ +""" + test_providers_monotonic + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.providers.monotonic` module. +""" +import time + +import pytest + +from ulid.providers import base, default, monotonic + +RANDOMNESS_OVERFLOW_REGEX = r'^Monotonic randomness value too large and will overflow' + + +@pytest.fixture(scope='function') +def provider(): + """ + Fixture that yields a monotonic provider instance. + """ + return monotonic.Provider(default.Provider()) + + +def test_provider_derives_from_base(): + """ + Assert that :class:`~ulid.providers.monotonic.Provider` derives from :class:`~ulid.providers.base.Provider`. + """ + assert issubclass(monotonic.Provider, base.Provider) + + +def test_provider_timestamp_returns_bytes(provider): + """ + Assert that :meth:`~ulid.providers.monotonic.Provider.timestamp` returns bytes of expected length. + """ + value = provider.timestamp() + assert isinstance(value, bytes) + assert len(value) == 6 + + +def test_provider_timestamp_uses_time_epoch(provider): + """ + Assert that :meth:`~ulid.providers.monotonic.Provider.timestamp` returns the current time milliseconds + since epoch in bytes. + """ + timestamp_bytes = provider.timestamp() + timestamp_int = int.from_bytes(timestamp_bytes, byteorder='big') + assert timestamp_int // 1000 < time.time() + + +def test_provider_randomness_returns_bytes(provider): + """ + Assert that :meth:`~ulid.providers.monotonic.Provider.randomness` returns bytes of expected length. + """ + value = provider.randomness(provider.timestamp()) + assert isinstance(value, bytes) + assert len(value) == 10 + + +def test_provider_randomness_returns_increasing_values_for_same_timestamp(provider): + """ + Assert that :meth:`~ulid.providers.monotonic.Provider.randomness` returns incremented random values + for matching timestamps. + """ + timestamp = provider.timestamp() + x = provider.randomness(timestamp) + y = provider.randomness(timestamp) + + assert x != y + + x_int = int.from_bytes(x, byteorder='big') + y_int = int.from_bytes(y, byteorder='big') + + assert x_int + 1 == y_int + + +def test_provider_randomness_raises_on_max_randomness(provider): + """ + Assert that :meth:`~ulid.providers.monotonic.Provider.randomness` raises a :class:`~ValueError` + when incrementing the randomness value for the same timestamp would overflow. + """ + timestamp = provider.timestamp() + + with pytest.raises(ValueError, match=RANDOMNESS_OVERFLOW_REGEX): + provider.randomness(timestamp) + provider.prev_randomness = monotonic.consts.MAX_RANDOMNESS + provider.randomness(timestamp) diff --git a/tests/test_providers_package.py b/tests/test_providers_package.py new file mode 100644 index 0000000..0b79f55 --- /dev/null +++ b/tests/test_providers_package.py @@ -0,0 +1,37 @@ +""" + test_providers_package + ~~~~~~~~~~~~~~~~~~~~~~ + + Tests for the :mod:`~ulid.providers` package. +""" +from ulid import providers +from ulid.providers import default, monotonic + + +def test_package_has_dunder_all(): + """ + Assert that :pkg:`~ulid.providers` exposes the :attr:`~ulid.providers.__all__` attribute as a list. + """ + assert hasattr(providers, '__all__') + assert isinstance(providers.__all__, list) + + +def test_package_exposes_expected_interface(): + """ + Assert that :attr:`~ulid.providers.__all__` exposes expected interface. + """ + assert providers.__all__ == ['Provider', 'DEFAULT', 'MONOTONIC'] + + +def test_package_has_default_provider(): + """ + Assert :attr:`~ulid.providers.DEFAULT` is a :class:`~ulid.providers.default.Provider` instance. + """ + assert isinstance(providers.DEFAULT, default.Provider) + + +def test_package_has_monotonic_provider(): + """ + Assert :attr:`~ulid.providers.MONOTONIC` is a :class:`~ulid.providers.monotonic.Provider` instance. + """ + assert isinstance(providers.MONOTONIC, monotonic.Provider) 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..0f40dad --- /dev/null +++ b/ulid/api/__init__.py @@ -0,0 +1,30 @@ +""" + ulid/api + ~~~~~~~~ + + Defines the public API of the `ulid` package. +""" +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 = 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 = default.Timestamp +Randomness = default.Randomness +ULID = default.ULID + +__all__ = default.__all__ diff --git a/ulid/api/api.py b/ulid/api/api.py new file mode 100644 index 0000000..d06d1da --- /dev/null +++ b/ulid/api/api.py @@ -0,0 +1,260 @@ +""" + 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 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..3d73ea0 --- /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: + curr_timestamp = ulid.Timestamp(timestamp) + + # Randomness requested for new timestamp. + if curr_timestamp > self.prev_timestamp: + self.prev_randomness = ulid.Randomness(self.default.randomness(curr_timestamp.bytes)) + self.prev_timestamp = curr_timestamp + + # 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