Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add microsecond provider #476

Merged
merged 1 commit into from
Sep 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions tests/test_api_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ def mock_provider(mocker):
Fixture that yields a mock provider.
"""
provider = mocker.Mock(spec=providers.Provider)
provider.new = mocker.Mock(side_effect=providers.DEFAULT.new)
provider.timestamp = mocker.Mock(side_effect=providers.DEFAULT.timestamp)
provider.randomness = mocker.Mock(side_effect=providers.DEFAULT.randomness)
return provider
Expand Down Expand Up @@ -55,22 +56,14 @@ def test_all_defined_expected_methods():
]


def test_api_new_calls_provider_timestamp(mock_api):
def test_api_new_calls_provider_new(mock_api):
"""
Assert :meth:`~ulid.api.api.Api.new` calls :meth:`~ulid.providers.base.Provider.timestamp` for a value.
Assert :meth:`~ulid.api.api.Api.new` calls :meth:`~ulid.providers.base.Provider.new` for timestamp
and randomness values.
"""
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)
mock_api.provider.new.assert_called_once_with()


def test_api_from_timestamp_calls_provider_randomness(mocker, mock_api, valid_bytes_48):
Expand Down
7 changes: 7 additions & 0 deletions tests/test_api_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ def test_module_exposes_expected_interface():
Assert that :attr:`~ulid.api.default.__all__` exposes expected interface.
"""
assert default.__all__ == ALL


def test_module_api_uses_correct_provider():
"""
Assert that the API instance uses the correct provider type.
"""
assert isinstance(default.API.provider, type(default.providers.DEFAULT))
30 changes: 30 additions & 0 deletions tests/test_api_microsecond.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
test_api_microsecond
~~~~~~~~~~~~~~~~~~~~

Tests for the :mod:`~ulid.api.microsecond` module.
"""
from ulid.api import microsecond
from ulid.api.api import ALL


def test_module_has_dunder_all():
"""
Assert that :mod:`~ulid.api.microsecond` exposes the :attr:`~ulid.api.__all__` attribute as a list.
"""
assert hasattr(microsecond, '__all__')
assert isinstance(microsecond.__all__, list)


def test_module_exposes_expected_interface():
"""
Assert that :attr:`~ulid.api.microsecond.__all__` exposes expected interface.
"""
assert microsecond.__all__ == ALL


def test_module_api_uses_correct_provider():
"""
Assert that the API instance uses the correct provider type.
"""
assert isinstance(microsecond.API.provider, type(microsecond.providers.MICROSECOND))
7 changes: 7 additions & 0 deletions tests/test_api_monotonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,10 @@ def test_module_exposes_expected_interface():
Assert that :attr:`~ulid.api.monotonic.__all__` exposes expected interface.
"""
assert monotonic.__all__ == ALL


def test_module_api_uses_correct_provider():
"""
Assert that the API instance uses the correct provider type.
"""
assert isinstance(monotonic.API.provider, type(monotonic.providers.MONOTONIC))
12 changes: 12 additions & 0 deletions tests/test_providers_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,18 @@ def test_provider_derives_from_base():
assert issubclass(default.Provider, base.Provider)


def test_provider_new_returns_bytes_pair(provider):
"""
Assert that :meth:`~ulid.providers.default.Provider.new` returns timestamp and randomness
bytes of expected length as a two item tuple.
"""
value = provider.new()
assert isinstance(value, tuple)
assert len(value) == 2
assert len(value[0]) == 6
assert len(value[1]) == 10


def test_provider_timestamp_returns_bytes(provider):
"""
Assert that :meth:`~ulid.providers.default.Provider.timestamp` returns bytes of expected length.
Expand Down
119 changes: 119 additions & 0 deletions tests/test_providers_microsecond.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""
test_providers_microsecond
~~~~~~~~~~~~~~~~~~~~~~~~~~

Tests for the :mod:`~ulid.providers.microsecond` module.
"""
import pytest

from ulid.providers import base, default, microsecond, time


@pytest.fixture(scope='function')
def provider():
"""
Fixture that yields a microsecond provider instance.
"""
return microsecond.Provider(default.Provider())


@pytest.fixture(scope='function')
def valid_epoch_milliseconds():
"""
Fixture that yields a epoch value in milliseconds.
"""
return time.PROVIDER.milliseconds()


@pytest.fixture(scope='function')
def valid_epoch_microseconds():
"""
Fixture that yields a epoch value in microseconds.
"""
return time.PROVIDER.microseconds()


@pytest.fixture(scope='function')
def mock_time_provider(mocker, valid_epoch_milliseconds, valid_epoch_microseconds):
"""
Fixture that yields a mock time provider.
"""
provider = mocker.Mock(spec=time.Provider)
provider.milliseconds = mocker.Mock(return_value=valid_epoch_milliseconds)
provider.microseconds = mocker.Mock(return_value=valid_epoch_microseconds)
return provider


@pytest.fixture(scope='function')
def provider_time_mock(mocker, mock_time_provider):
"""
Fixture that yields a provider with time mock.
"""
mocker.patch.object(microsecond.time, 'milliseconds', side_effect=mock_time_provider.milliseconds)
mocker.patch.object(microsecond.time, 'microseconds', side_effect=mock_time_provider.microseconds)
return microsecond.Provider(default.Provider())


def test_provider_derives_from_base():
"""
Assert that :class:`~ulid.providers.microsecond.Provider` derives from :class:`~ulid.providers.base.Provider`.
"""
assert issubclass(microsecond.Provider, base.Provider)


def test_provider_new_returns_bytes_pair(provider):
"""
Assert that :meth:`~ulid.providers.microsecond.Provider.new` returns timestamp and randomness
bytes of expected length as a two item tuple.
"""
value = provider.new()
assert isinstance(value, tuple)
assert len(value) == 2
assert len(value[0]) == 6
assert len(value[1]) == 10


def test_provider_new_returns_randomness_with_microseconds(provider_time_mock):
"""
Assert that :meth:`~ulid.providers.default.Provider.new` returns timestamp and randomness
bytes that use microseconds as the first two bytes of randomness.
"""
epoch_us = time.microseconds()
epoch_ms = epoch_us // 1000
microseconds = epoch_us % epoch_ms
microseconds_bits = microseconds << 6

_, randomness = provider_time_mock.new()

prefix = int.from_bytes(randomness[:2], byteorder='big')
microsecond_prefix_bits = (prefix >> 6) << 6

assert microsecond_prefix_bits == microseconds_bits


def test_provider_timestamp_returns_bytes(provider):
"""
Assert that :meth:`~ulid.providers.microsecond.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.microsecond.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 <= time.milliseconds()


def test_provider_randomness_returns_bytes(provider):
"""
Assert that :meth:`~ulid.providers.microsecond.Provider.randomness` returns bytes of expected length.
"""
value = provider.randomness(provider.timestamp())
assert isinstance(value, bytes)
assert len(value) == 10
12 changes: 12 additions & 0 deletions tests/test_providers_monotonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ def test_provider_derives_from_base():
assert issubclass(monotonic.Provider, base.Provider)


def test_provider_new_returns_bytes_pair(provider):
"""
Assert that :meth:`~ulid.providers.monotonic.Provider.new` returns timestamp and randomness
bytes of expected length as a two item tuple.
"""
value = provider.new()
assert isinstance(value, tuple)
assert len(value) == 2
assert len(value[0]) == 6
assert len(value[1]) == 10


def test_provider_timestamp_returns_bytes(provider):
"""
Assert that :meth:`~ulid.providers.monotonic.Provider.timestamp` returns bytes of expected length.
Expand Down
2 changes: 1 addition & 1 deletion tests/test_providers_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_package_exposes_expected_interface():
"""
Assert that :attr:`~ulid.providers.__all__` exposes expected interface.
"""
assert providers.__all__ == ['Provider', 'DEFAULT', 'MONOTONIC']
assert providers.__all__ == ['Provider', 'DEFAULT', 'MICROSECOND', 'MONOTONIC']


def test_package_has_default_provider():
Expand Down
4 changes: 4 additions & 0 deletions tests/test_providers_time_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ def test_provider_milliseconds_is_unix_epoch(provider):
since epoch.
"""
x = int(time.time() * 1000)
time.sleep(1)
y = provider.milliseconds()
time.sleep(1)
z = int(time.time() * 1000)

assert x <= y <= z
Expand All @@ -60,7 +62,9 @@ def test_provider_microseconds_is_unix_epoch(provider):
since epoch.
"""
x = int(time.time() * 1000 * 1000)
time.sleep(1)
y = provider.microseconds()
time.sleep(1)
z = int(time.time() * 1000 * 1000)

assert x <= y <= z
4 changes: 4 additions & 0 deletions tests/test_providers_time_nanosecond.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def test_provider_milliseconds_is_unix_epoch(provider):
since epoch.
"""
x = int(time.time() * 1000)
time.sleep(1)
y = provider.milliseconds()
time.sleep(1)
z = int(time.time() * 1000)

assert x <= y <= z
Expand All @@ -64,7 +66,9 @@ def test_provider_microseconds_is_unix_epoch(provider):
since epoch.
"""
x = int(time.time() * 1000 * 1000)
time.sleep(1)
y = provider.microseconds()
time.sleep(1)
z = int(time.time() * 1000 * 1000)

assert x <= y <= z
2 changes: 1 addition & 1 deletion ulid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
:copyright: (c) 2017 Andrew Hawker.
:license: Apache 2.0, see LICENSE for more details.
"""
from .api import default, monotonic
from .api import default, microsecond, monotonic

create = default.create
from_bytes = default.from_bytes
Expand Down
3 changes: 1 addition & 2 deletions ulid/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,7 @@ def new(self) -> ulid.ULID:
:return: ULID from current timestamp
:rtype: :class:`~ulid.ulid.ULID`
"""
timestamp = self.provider.timestamp()
randomness = self.provider.randomness(timestamp)
timestamp, randomness = self.provider.new()
return ulid.ULID(timestamp + randomness)

def parse(self, value: ULIDPrimitive) -> ulid.ULID:
Expand Down
2 changes: 1 addition & 1 deletion ulid/api/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
ulid/api/default
~~~~~~~~~~~~~~~~

Defaults the public API of the `ulid` package using the default provider.
Contains the public API of the `ulid` package using the default provider.
"""
from .. import consts, providers, ulid
from . import api
Expand Down
33 changes: 33 additions & 0 deletions ulid/api/microsecond.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
ulid/api/microsecond
~~~~~~~~~~~~~~~~~~~~

Contains the public API of the `ulid` package using the microsecond provider.
"""
from .. import consts, providers, ulid
from . import api

API = api.Api(providers.MICROSECOND)

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
2 changes: 1 addition & 1 deletion ulid/api/monotonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
ulid/api/monotonic
~~~~~~~~~~~~~~~~~~

Defaults the public API of the `ulid` package using a monotonic randomness provider.
Contains the public API of the `ulid` package using the monotonic provider.
"""
from .. import consts, providers, ulid
from . import api
Expand Down
5 changes: 3 additions & 2 deletions ulid/providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
Contains functionality for timestamp/randomness data providers.
"""

from . import base, default, monotonic
from . import base, default, microsecond, monotonic

Provider = base.Provider
DEFAULT = default.Provider()
MICROSECOND = microsecond.Provider(DEFAULT)
MONOTONIC = monotonic.Provider(DEFAULT)

__all__ = ['Provider', 'DEFAULT', 'MONOTONIC']
__all__ = ['Provider', 'DEFAULT', 'MICROSECOND', 'MONOTONIC']
Loading