From e6e96be257817e73b128d0740e362ee99f141cf8 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 11:33:57 +0200 Subject: [PATCH 01/35] Added all types of generators. --- ulid/value_provider/__init__.py | 3 ++ .../value_provider/abstract_value_provider.py | 23 ++++++++++++++ .../monotonic_value_provider.py | 30 +++++++++++++++++++ .../non_monotonic_value_provider.py | 5 ++++ 4 files changed, 61 insertions(+) create mode 100644 ulid/value_provider/__init__.py create mode 100644 ulid/value_provider/abstract_value_provider.py create mode 100644 ulid/value_provider/monotonic_value_provider.py create mode 100644 ulid/value_provider/non_monotonic_value_provider.py diff --git a/ulid/value_provider/__init__.py b/ulid/value_provider/__init__.py new file mode 100644 index 0000000..67026b3 --- /dev/null +++ b/ulid/value_provider/__init__.py @@ -0,0 +1,3 @@ +from .abstract_value_provider import AbstractValueProvider +from .monotonic_value_provider import MonotonicValueProvider +from .non_monotonic_value_provider import NonMonotonicValueProvider \ No newline at end of file diff --git a/ulid/value_provider/abstract_value_provider.py b/ulid/value_provider/abstract_value_provider.py new file mode 100644 index 0000000..091b251 --- /dev/null +++ b/ulid/value_provider/abstract_value_provider.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +import os +import time + +from ulid import constants + + +class AbstractValueProvider(ABC): + def timestamp(self, value: float | None = None) -> int: + if value is None: + value = time.time_ns() // constants.NANOSECS_IN_MILLISECS + elif isinstance(value, float): + value = int(value * constants.MILLISECS_IN_SECS) + if value > constants.MAX_TIMESTAMP: + raise ValueError("Value exceeds maximum possible timestamp") + return value + + @abstractmethod + def randomness(self) -> bytes: + pass + + def _generate_randomness(self) -> bytes: + return os.urandom(constants.RANDOMNESS_LEN) \ No newline at end of file diff --git a/ulid/value_provider/monotonic_value_provider.py b/ulid/value_provider/monotonic_value_provider.py new file mode 100644 index 0000000..c16de28 --- /dev/null +++ b/ulid/value_provider/monotonic_value_provider.py @@ -0,0 +1,30 @@ +import time +import os +from threading import Lock + +from ulid import constants +from ulid.value_provider.abstract_value_provider import AbstractValueProvider + +class MonotonicValueProvider(AbstractValueProvider): + def __init__(self) -> None: + self.lock = Lock() + self.prev_timestamp = constants.MIN_TIMESTAMP + self.prev_randomness = constants.MIN_RANDOMNESS + + def randomness(self) -> bytes: + with self.lock: + current_timestamp = self.timestamp() + if current_timestamp == self.prev_timestamp: + if self.prev_randomness == constants.MAX_RANDOMNESS: + raise ValueError("Randomness within same millisecond exhausted") + randomness = self.increment_bytes(self.prev_randomness) + else: + randomness = self._generate_randomness() + + self.prev_randomness = randomness + self.prev_timestamp = current_timestamp + return randomness + + def increment_bytes(self, value: bytes) -> bytes: + length = len(value) + return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big") \ No newline at end of file diff --git a/ulid/value_provider/non_monotonic_value_provider.py b/ulid/value_provider/non_monotonic_value_provider.py new file mode 100644 index 0000000..f0b062d --- /dev/null +++ b/ulid/value_provider/non_monotonic_value_provider.py @@ -0,0 +1,5 @@ +from .abstract_value_provider import AbstractValueProvider + +class NonMonotonicValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + return self._generate_randomness() \ No newline at end of file From 502dc2f558b6655a494e67b2a679ce46c1af3866 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 11:40:01 +0200 Subject: [PATCH 02/35] Used the value provider from the new file. --- ulid/__init__.py | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index d327385..d74c1be 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -17,6 +17,7 @@ from ulid import base32 from ulid import constants +from ulid.value_provider import MonotonicValueProvider as ValueProvider if TYPE_CHECKING: # pragma: no cover @@ -53,41 +54,6 @@ def wrapped(cls: Any, value: T) -> R: return wrapped - -class ValueProvider: - def __init__(self) -> None: - self.lock = Lock() - self.prev_timestamp = constants.MIN_TIMESTAMP - self.prev_randomness = constants.MIN_RANDOMNESS - - def timestamp(self, value: float | None = None) -> int: - if value is None: - value = time.time_ns() // constants.NANOSECS_IN_MILLISECS - elif isinstance(value, float): - value = int(value * constants.MILLISECS_IN_SECS) - if value > constants.MAX_TIMESTAMP: - raise ValueError("Value exceeds maximum possible timestamp") - return value - - def randomness(self) -> bytes: - with self.lock: - current_timestamp = self.timestamp() - if current_timestamp == self.prev_timestamp: - if self.prev_randomness == constants.MAX_RANDOMNESS: - raise ValueError("Randomness within same millisecond exhausted") - randomness = self.increment_bytes(self.prev_randomness) - else: - randomness = os.urandom(constants.RANDOMNESS_LEN) - - self.prev_randomness = randomness - self.prev_timestamp = current_timestamp - return randomness - - def increment_bytes(self, value: bytes) -> bytes: - length = len(value) - return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big") - - @functools.total_ordering class ULID: provider = ValueProvider() From 7c106bceedb38dea59e36745d1bb00c8d6113e16 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 11:48:52 +0200 Subject: [PATCH 03/35] Added pytest as a dependency. --- hatch.toml | 1 + pyproject.toml | 1 + uv.lock | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) diff --git a/hatch.toml b/hatch.toml index e0fd336..75744be 100644 --- a/hatch.toml +++ b/hatch.toml @@ -11,6 +11,7 @@ config-path = ".ruff_defaults.toml" [envs.hatch-test] extra-dependencies = [ "freezegun==1.5.*", + "pytest==8.4.*", ] features = [ "pydantic" diff --git a/pyproject.toml b/pyproject.toml index 5c3b252..58159f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,6 +107,7 @@ max-line-length = 100 [tool.uv] dev-dependencies = [ + "pytest>=8.4", "hatch>=1.14.1", "freezegun>=1.5", "sphinx>=7.4.7", diff --git a/uv.lock b/uv.lock index 9818362..4ef6c2c 100644 --- a/uv.lock +++ b/uv.lock @@ -438,6 +438,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jaraco-classes" version = "3.4.0" @@ -810,6 +835,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pytest" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.10'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, + { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "packaging", marker = "python_full_version < '3.10'" }, + { name = "pluggy", marker = "python_full_version < '3.10'" }, + { name = "pygments", marker = "python_full_version < '3.10'" }, + { name = "tomli", marker = "python_full_version < '3.10'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version == '3.10.*'", +] +dependencies = [ + { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, + { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "packaging", marker = "python_full_version >= '3.10'" }, + { name = "pluggy", marker = "python_full_version >= '3.10'" }, + { name = "pygments", marker = "python_full_version >= '3.10'" }, + { name = "tomli", marker = "python_full_version == '3.10.*'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -835,6 +903,8 @@ pydantic = [ dev = [ { name = "freezegun" }, { name = "hatch" }, + { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -848,6 +918,7 @@ provides-extras = ["pydantic"] dev = [ { name = "freezegun", specifier = ">=1.5" }, { name = "hatch", specifier = ">=1.14.1" }, + { name = "pytest", specifier = ">=8.4.0" }, { name = "sphinx", specifier = ">=7.4.7" }, ] From ba852e20fd2a174a881516d610b0509e08773280 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 11:57:19 +0200 Subject: [PATCH 04/35] Updated same milisecond overflow, updated the timestamp of the previous. --- tests/test_ulid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 11ae933..7a8f8bb 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -70,6 +70,7 @@ def test_same_millisecond_monotonic_sorting() -> None: @freeze_time() def test_same_millisecond_overflow() -> None: ULID.provider.prev_randomness = constants.MAX_RANDOMNESS + ULID.provider.prev_timestamp = ULID.provider.timestamp() with pytest.raises(ValueError, match="Randomness within same millisecond exhausted"): ULID() From 6076ce06f49faec5b8ead0216ed6ea11a5ecd560 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:14:32 +0200 Subject: [PATCH 05/35] Replaced the validate value type decorator with a function that justs validates. --- ulid/__init__.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index d74c1be..5b85d98 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -39,20 +39,11 @@ R = TypeVar("R") -class validate_type(Generic[T]): # noqa: N801 - def __init__(self, *types: T) -> None: - self.types = types - - def __call__(self, func: Callable[..., R]) -> Callable[..., R]: - @functools.wraps(func) - def wrapped(cls: Any, value: T) -> R: - if not isinstance(value, self.types): - message = "Value has to be of type " - message += " or ".join([t.__name__ for t in self.types]) - raise TypeError(message) - return func(cls, value) - - return wrapped +def validate_value_type(type_to_validate: type, *types_to_validate_against: type,) -> None: + if not isinstance(type_to_validate, types_to_validate_against): + message = "Value has to be of type " + message += " or ".join([t.__name__ for t in types_to_validate_against]) + raise TypeError(message) @functools.total_ordering class ULID: @@ -88,7 +79,6 @@ def __init__(self, value: bytes | None = None) -> None: self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes @classmethod - @validate_type(datetime) def from_datetime(cls, value: datetime) -> Self: """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the `ULID` will be set to the corresponding timestamp of the datetime. @@ -99,10 +89,10 @@ def from_datetime(cls, value: datetime) -> Self: >>> ULID.from_datetime(datetime.now()) ULID(01E75QRYCAMM1MKQ9NYMYT6SAV) """ + validate_value_type(value, datetime) return cls.from_timestamp(value.timestamp()) @classmethod - @validate_type(int, float) def from_timestamp(cls, value: float) -> Self: """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a `float` representing the time in seconds (as it would be returned by :func:`time.time()`) @@ -114,6 +104,7 @@ def from_timestamp(cls, value: float) -> Self: >>> ULID.from_timestamp(time.time()) ULID(01E75QWN5HKQ0JAVX9FG1K4YP4) """ + validate_value_type(value, int, float) timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big") randomness = cls.provider.randomness() return cls.from_bytes(timestamp + randomness) @@ -130,30 +121,31 @@ def from_uuid(cls, value: uuid.UUID) -> Self: >>> ULID.from_uuid(uuid4()) ULID(27Q506DP7E9YNRXA0XVD8Z5YSG) """ + validate_value_type(value, uuid.UUID) return cls(value.bytes) @classmethod - @validate_type(bytes) def from_bytes(cls, bytes_: bytes) -> Self: """Create a new :class:`ULID`-object from sequence of 16 bytes.""" + validate_value_type(bytes_, bytes) return cls(bytes_) @classmethod - @validate_type(str) def from_hex(cls, value: str) -> Self: """Create a new :class:`ULID`-object from 32 character string of hex values.""" + validate_value_type(value, str) return cls.from_bytes(bytes.fromhex(value)) @classmethod - @validate_type(str) def from_str(cls, string: str) -> Self: """Create a new :class:`ULID`-object from a 26 char long string representation.""" + validate_value_type(string, str) return cls(base32.decode(string)) @classmethod - @validate_type(int) def from_int(cls, value: int) -> Self: """Create a new :class:`ULID`-object from an `int`.""" + validate_value_type(value, int) return cls(int.to_bytes(value, constants.BYTES_LEN, "big")) @classmethod From 08478af2cb95288a4dec85f4148a1b89fd9701e8 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:15:42 +0200 Subject: [PATCH 06/35] Forgot to remove certain decorator. --- ulid/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 5b85d98..4a639d4 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -110,7 +110,6 @@ def from_timestamp(cls, value: float) -> Self: return cls.from_bytes(timestamp + randomness) @classmethod - @validate_type(uuid.UUID) def from_uuid(cls, value: uuid.UUID) -> Self: """Create a new :class:`ULID`-object from a :class:`uuid.UUID`. The timestamp part will be random in that case. From 47ec83a7155e272e42e4cf2102c9350b5f00349e Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:16:57 +0200 Subject: [PATCH 07/35] Removed unecessary types. --- ulid/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 4a639d4..7e6d2ed 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -35,9 +35,6 @@ __version__ = version("python-ulid") -T = TypeVar("T", bound=type) -R = TypeVar("R") - def validate_value_type(type_to_validate: type, *types_to_validate_against: type,) -> None: if not isinstance(type_to_validate, types_to_validate_against): From 32b3781d6fe60b8b1ccfeb329d6625e7caa52cc3 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:17:35 +0200 Subject: [PATCH 08/35] Removed unused imports. --- ulid/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 7e6d2ed..5c9ae47 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -1,17 +1,12 @@ from __future__ import annotations import functools -import os -import time import uuid from datetime import datetime from datetime import timezone -from threading import Lock from typing import Any from typing import cast -from typing import Generic from typing import TYPE_CHECKING -from typing import TypeVar from typing_extensions import Self From 937d9877d73c088eb3a3935ff271ff0517385bed Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:17:55 +0200 Subject: [PATCH 09/35] Forgot one unused import. --- ulid/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 5c9ae47..a5f647a 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -16,8 +16,6 @@ if TYPE_CHECKING: # pragma: no cover - from collections.abc import Callable - from pydantic import GetCoreSchemaHandler from pydantic import ValidatorFunctionWrapHandler from pydantic_core import CoreSchema From 17ebb4c54b1c5daf9a0490ecf8c8820f8dcfb478 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:39:28 +0200 Subject: [PATCH 10/35] Added test for other value provider at the c'tor. --- tests/test_ulid.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 7a8f8bb..84882d6 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -16,6 +16,7 @@ from ulid import base32 from ulid import constants from ulid import ULID +from ulid.value_provider.abstract_value_provider import AbstractValueProvider def utcnow() -> datetime: @@ -276,3 +277,25 @@ class Model(BaseModel): assert { "type": "null", } in model_json_schema["properties"]["ulid"]["anyOf"] + +def test_ulid_support_other_value_provider() -> None: + random_part: bytes = b"\x00" * 10 + datetime = utcnow() + timestamp_in_seconds = int(datetime.timestamp()) + timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) + ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + class DummyValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + return random_part + + def timestamp(self, value: float | None = None) -> int: + return timestamp_in_milliseconds + + ulid = ULID(value_provider=DummyValueProvider()) + + assert ulid.bytes == ulid_bytes + assert ulid.timestamp == timestamp_in_seconds + datetimes_almost_equal(ulid.datetime, datetime) + assert ulid.milliseconds == timestamp_in_milliseconds + assert ulid.hex == ulid_bytes.hex() + assert str(ulid) == base32.encode(ulid_bytes) From b9716caf0561c586ba9105c362aa0b00a8a3d14b Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:39:44 +0200 Subject: [PATCH 11/35] Updated the implementation to work that way as well. --- ulid/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index a5f647a..96bd326 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -63,10 +63,11 @@ class ULID: ValueError: If the provided value is not a valid encoded ULID. """ - def __init__(self, value: bytes | None = None) -> None: + def __init__(self, value: bytes | None = None, value_provider: ValueProvider | None = None) -> None: if value is not None and len(value) != constants.BYTES_LEN: raise ValueError("ULID has to be exactly 16 bytes long.") - self.bytes: bytes = value or ULID.from_timestamp(self.provider.timestamp()).bytes + value_provider_to_use = value_provider or self.provider + self.bytes: bytes = value or ULID.from_timestamp(value_provider_to_use.timestamp(), value_provider=value_provider_to_use).bytes @classmethod def from_datetime(cls, value: datetime) -> Self: @@ -83,7 +84,7 @@ def from_datetime(cls, value: datetime) -> Self: return cls.from_timestamp(value.timestamp()) @classmethod - def from_timestamp(cls, value: float) -> Self: + def from_timestamp(cls, value: float, value_provider: ValueProvider | None = None) -> Self: """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a `float` representing the time in seconds (as it would be returned by :func:`time.time()`) or an `int` in milliseconds. @@ -95,8 +96,9 @@ def from_timestamp(cls, value: float) -> Self: ULID(01E75QWN5HKQ0JAVX9FG1K4YP4) """ validate_value_type(value, int, float) - timestamp = int.to_bytes(cls.provider.timestamp(value), constants.TIMESTAMP_LEN, "big") - randomness = cls.provider.randomness() + value_provider_to_use = value_provider or cls.provider + timestamp = int.to_bytes(value_provider_to_use.timestamp(value), constants.TIMESTAMP_LEN, "big") + randomness = value_provider_to_use.randomness() return cls.from_bytes(timestamp + randomness) @classmethod From 9ba1acf53b9b9c5cbd2193a576da35e3001ae498 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:41:56 +0200 Subject: [PATCH 12/35] Removed private method at the abstarct value provider. --- ulid/value_provider/abstract_value_provider.py | 3 --- ulid/value_provider/monotonic_value_provider.py | 2 +- ulid/value_provider/non_monotonic_value_provider.py | 6 +++++- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ulid/value_provider/abstract_value_provider.py b/ulid/value_provider/abstract_value_provider.py index 091b251..8a81ab6 100644 --- a/ulid/value_provider/abstract_value_provider.py +++ b/ulid/value_provider/abstract_value_provider.py @@ -18,6 +18,3 @@ def timestamp(self, value: float | None = None) -> int: @abstractmethod def randomness(self) -> bytes: pass - - def _generate_randomness(self) -> bytes: - return os.urandom(constants.RANDOMNESS_LEN) \ No newline at end of file diff --git a/ulid/value_provider/monotonic_value_provider.py b/ulid/value_provider/monotonic_value_provider.py index c16de28..4e15ecf 100644 --- a/ulid/value_provider/monotonic_value_provider.py +++ b/ulid/value_provider/monotonic_value_provider.py @@ -19,7 +19,7 @@ def randomness(self) -> bytes: raise ValueError("Randomness within same millisecond exhausted") randomness = self.increment_bytes(self.prev_randomness) else: - randomness = self._generate_randomness() + randomness = os.urandom(constants.RANDOMNESS_LEN) self.prev_randomness = randomness self.prev_timestamp = current_timestamp diff --git a/ulid/value_provider/non_monotonic_value_provider.py b/ulid/value_provider/non_monotonic_value_provider.py index f0b062d..cefc95f 100644 --- a/ulid/value_provider/non_monotonic_value_provider.py +++ b/ulid/value_provider/non_monotonic_value_provider.py @@ -1,5 +1,9 @@ +import os + +from ulid import constants + from .abstract_value_provider import AbstractValueProvider class NonMonotonicValueProvider(AbstractValueProvider): def randomness(self) -> bytes: - return self._generate_randomness() \ No newline at end of file + return os.urandom(constants.RANDOMNESS_LEN) \ No newline at end of file From 75dab15fb6d6f25b103b8201bb73999425863fcf Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:48:00 +0200 Subject: [PATCH 13/35] Added tests for multiple ulids and for `from_timestamp` and `from_datetime`. --- tests/test_ulid.py | 74 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 84882d6..233422d 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -278,8 +278,8 @@ class Model(BaseModel): "type": "null", } in model_json_schema["properties"]["ulid"]["anyOf"] -def test_ulid_support_other_value_provider() -> None: - random_part: bytes = b"\x00" * 10 +def test_ulid_constructor_support_other_value_provider() -> None: + random_part = b"\x00" * 10 datetime = utcnow() timestamp_in_seconds = int(datetime.timestamp()) timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) @@ -291,11 +291,67 @@ def randomness(self) -> bytes: def timestamp(self, value: float | None = None) -> int: return timestamp_in_milliseconds - ulid = ULID(value_provider=DummyValueProvider()) + ulid1 = ULID(value_provider=DummyValueProvider()) + ulid2 = ULID(value_provider=DummyValueProvider()) + + assert ulid1.bytes == ulid_bytes + assert ulid1.timestamp == timestamp_in_seconds + datetimes_almost_equal(ulid1.datetime, datetime) + assert ulid1.milliseconds == timestamp_in_milliseconds + assert ulid1.hex == ulid_bytes.hex() + assert str(ulid1) == base32.encode(ulid_bytes) + # since the same dummy value provider is used, + # the generated ULIDs should be the same. + assert ulid2 == ulid1 + +def test_ulid_from_datetime_support_other_value_provider() -> None: + random_part = b"\x00" * 10 + datetime = utcnow() + timestamp_in_seconds = int(datetime.timestamp()) + timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) + ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + class DummyValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + return random_part + + def timestamp(self, value: float | None = None) -> int: + return timestamp_in_milliseconds + + ulid1 = ULID.from_datetime(datetime, value_provider=DummyValueProvider()) + ulid2 = ULID.from_datetime(datetime, value_provider=DummyValueProvider()) + + assert ulid1.bytes == ulid_bytes + assert ulid1.timestamp == timestamp_in_seconds + datetimes_almost_equal(ulid1.datetime, datetime) + assert ulid1.milliseconds == timestamp_in_milliseconds + assert ulid1.hex == ulid_bytes.hex() + assert str(ulid1) == base32.encode(ulid_bytes) + # since the same dummy value provider is used, + # the generated ULIDs should be the same. + assert ulid2 == ulid1 + +def test_ulid_from_timestamp_support_other_value_provider() -> None: + random_part = b"\x00" * 10 + datetime = utcnow() + timestamp_in_seconds = int(datetime.timestamp()) + timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) + ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + class DummyValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + return random_part + + def timestamp(self, value: float | None = None) -> int: + return timestamp_in_milliseconds - assert ulid.bytes == ulid_bytes - assert ulid.timestamp == timestamp_in_seconds - datetimes_almost_equal(ulid.datetime, datetime) - assert ulid.milliseconds == timestamp_in_milliseconds - assert ulid.hex == ulid_bytes.hex() - assert str(ulid) == base32.encode(ulid_bytes) + ulid1 = ULID.from_timestamp(datetime.timestamp(), value_provider=DummyValueProvider()) + ulid2 = ULID.from_timestamp(datetime.timestamp(), value_provider=DummyValueProvider()) + + assert ulid1.bytes == ulid_bytes + assert ulid1.timestamp == timestamp_in_seconds + datetimes_almost_equal(ulid1.datetime, datetime) + assert ulid1.milliseconds == timestamp_in_milliseconds + assert ulid1.hex == ulid_bytes.hex() + assert str(ulid1) == base32.encode(ulid_bytes) + # since the same dummy value provider is used, + # the generated ULIDs should be the same. + assert ulid2 == ulid1 \ No newline at end of file From 8bc117ad0b41c21022836d22bc0d1a133b572c3c Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 12:48:44 +0200 Subject: [PATCH 14/35] Updated the implementation. --- ulid/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 96bd326..9690778 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -70,7 +70,7 @@ def __init__(self, value: bytes | None = None, value_provider: ValueProvider | N self.bytes: bytes = value or ULID.from_timestamp(value_provider_to_use.timestamp(), value_provider=value_provider_to_use).bytes @classmethod - def from_datetime(cls, value: datetime) -> Self: + def from_datetime(cls, value: datetime, value_provider: ValueProvider | None = None) -> Self: """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the `ULID` will be set to the corresponding timestamp of the datetime. @@ -80,8 +80,9 @@ def from_datetime(cls, value: datetime) -> Self: >>> ULID.from_datetime(datetime.now()) ULID(01E75QRYCAMM1MKQ9NYMYT6SAV) """ + value_provider_to_use = value_provider or cls.provider validate_value_type(value, datetime) - return cls.from_timestamp(value.timestamp()) + return cls.from_timestamp(value.timestamp(), value_provider=value_provider_to_use) @classmethod def from_timestamp(cls, value: float, value_provider: ValueProvider | None = None) -> Self: From d0ffd6f9b2176771a19f22e84e490c22ec4b354e Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 16:36:57 +0200 Subject: [PATCH 15/35] Moved the utcnow and datetime almost equal to conftest. --- tests/conftest.py | 9 +++++++++ tests/test_ulid.py | 8 +------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 tests/conftest.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..1552a0c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,9 @@ +from datetime import datetime, timezone + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +def datetimes_almost_equal(a: datetime, b: datetime) -> None: + assert a.replace(microsecond=0) == b.replace(microsecond=0) \ No newline at end of file diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 233422d..8f3a885 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -18,13 +18,7 @@ from ulid import ULID from ulid.value_provider.abstract_value_provider import AbstractValueProvider - -def utcnow() -> datetime: - return datetime.now(timezone.utc) - - -def datetimes_almost_equal(a: datetime, b: datetime) -> None: - assert a.replace(microsecond=0) == b.replace(microsecond=0) +from tests.conftest import datetimes_almost_equal, utcnow @freeze_time() From 6630c0fdb6a3bb8e384b33842baf49de9da79818 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 16:44:43 +0200 Subject: [PATCH 16/35] Added tests for the abstract value provider. --- tests/value_provider/__init__.py | 0 .../test_abstract_value_provider.py | 64 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 tests/value_provider/__init__.py create mode 100644 tests/value_provider/test_abstract_value_provider.py diff --git a/tests/value_provider/__init__.py b/tests/value_provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/value_provider/test_abstract_value_provider.py b/tests/value_provider/test_abstract_value_provider.py new file mode 100644 index 0000000..08866b4 --- /dev/null +++ b/tests/value_provider/test_abstract_value_provider.py @@ -0,0 +1,64 @@ +# TODO: Test the value providers. +# TODO: Add docstrings to the value providers. + +from datetime import datetime, timezone + +import pytest + +from freezegun import freeze_time + +from tests.conftest import utcnow +from ulid import constants +from ulid.value_provider.abstract_value_provider import AbstractValueProvider + + +class TestValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + return b'\x00' * 10 + + +@pytest.mark.parametrize( + "datetime_timestamp", + [ + datetime(2026, 6, 6, 6, 6, 6, 6, tzinfo=timezone.utc), + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2025, 12, 31, 9, 6, 3, tzinfo=timezone.utc), + ], +) +def test_timestamp(datetime_timestamp: datetime) -> None: + provider = TestValueProvider() + expected_timestamp = int(datetime_timestamp.timestamp() * constants.MILLISECS_IN_SECS) + + first_timestamp = provider.timestamp(datetime_timestamp.timestamp()) + second_timestamp = provider.timestamp(datetime_timestamp.timestamp()) + + assert isinstance(first_timestamp, int) + assert first_timestamp == expected_timestamp + assert isinstance(second_timestamp, int) + assert second_timestamp == expected_timestamp + assert second_timestamp == first_timestamp + + +@freeze_time() +def test_timestamp_now() -> None: + provider = TestValueProvider() + + with freeze_time() as frozen: + expected_first_timestamp = int(utcnow().timestamp() * constants.MILLISECS_IN_SECS) + first_timestamp = provider.timestamp() + frozen.tick() + expected_second_timestamp = int(utcnow().timestamp() * constants.MILLISECS_IN_SECS) + second_timestamp = provider.timestamp() + + assert isinstance(first_timestamp, int) + assert first_timestamp == expected_first_timestamp + assert isinstance(second_timestamp, int) + assert second_timestamp == expected_second_timestamp + assert second_timestamp > first_timestamp + + +def test_max_timestamp() -> None: + provider = TestValueProvider() + + with pytest.raises(ValueError, match="Value exceeds maximum possible timestamp"): + provider.timestamp(constants.MAX_TIMESTAMP + 1) From add528ae2988f48caab0f6b86101de97af42dc1e Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 16:50:45 +0200 Subject: [PATCH 17/35] Added the same timestamp tests for many value providers. --- ... => test_timestamp_value_base_provider.py} | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) rename tests/value_provider/{test_abstract_value_provider.py => test_timestamp_value_base_provider.py} (50%) diff --git a/tests/value_provider/test_abstract_value_provider.py b/tests/value_provider/test_timestamp_value_base_provider.py similarity index 50% rename from tests/value_provider/test_abstract_value_provider.py rename to tests/value_provider/test_timestamp_value_base_provider.py index 08866b4..1f14cad 100644 --- a/tests/value_provider/test_abstract_value_provider.py +++ b/tests/value_provider/test_timestamp_value_base_provider.py @@ -10,6 +10,8 @@ from tests.conftest import utcnow from ulid import constants from ulid.value_provider.abstract_value_provider import AbstractValueProvider +from ulid.value_provider.monotonic_value_provider import MonotonicValueProvider +from ulid.value_provider.non_monotonic_value_provider import NonMonotonicValueProvider class TestValueProvider(AbstractValueProvider): @@ -25,12 +27,19 @@ def randomness(self) -> bytes: datetime(2025, 12, 31, 9, 6, 3, tzinfo=timezone.utc), ], ) -def test_timestamp(datetime_timestamp: datetime) -> None: - provider = TestValueProvider() +@pytest.mark.parametrize( + "value_provider", + [ + pytest.param(TestValueProvider(), id="CustomedValueProvider"), + pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") + ] +) +def test_timestamp(datetime_timestamp: datetime, value_provider: AbstractValueProvider,) -> None: expected_timestamp = int(datetime_timestamp.timestamp() * constants.MILLISECS_IN_SECS) - first_timestamp = provider.timestamp(datetime_timestamp.timestamp()) - second_timestamp = provider.timestamp(datetime_timestamp.timestamp()) + first_timestamp = value_provider.timestamp(datetime_timestamp.timestamp()) + second_timestamp = value_provider.timestamp(datetime_timestamp.timestamp()) assert isinstance(first_timestamp, int) assert first_timestamp == expected_timestamp @@ -40,15 +49,21 @@ def test_timestamp(datetime_timestamp: datetime) -> None: @freeze_time() -def test_timestamp_now() -> None: - provider = TestValueProvider() - +@pytest.mark.parametrize( + "value_provider", + [ + pytest.param(TestValueProvider(), id="CustomedValueProvider"), + pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") + ] +) +def test_timestamp_now(value_provider: AbstractValueProvider,) -> None: with freeze_time() as frozen: expected_first_timestamp = int(utcnow().timestamp() * constants.MILLISECS_IN_SECS) - first_timestamp = provider.timestamp() + first_timestamp = value_provider.timestamp() frozen.tick() expected_second_timestamp = int(utcnow().timestamp() * constants.MILLISECS_IN_SECS) - second_timestamp = provider.timestamp() + second_timestamp = value_provider.timestamp() assert isinstance(first_timestamp, int) assert first_timestamp == expected_first_timestamp @@ -57,8 +72,14 @@ def test_timestamp_now() -> None: assert second_timestamp > first_timestamp -def test_max_timestamp() -> None: - provider = TestValueProvider() - +@pytest.mark.parametrize( + "value_provider", + [ + pytest.param(TestValueProvider(), id="CustomedValueProvider"), + pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") + ] +) +def test_max_timestamp(value_provider: AbstractValueProvider,) -> None: with pytest.raises(ValueError, match="Value exceeds maximum possible timestamp"): - provider.timestamp(constants.MAX_TIMESTAMP + 1) + value_provider.timestamp(constants.MAX_TIMESTAMP + 1) From ea3d4466ecd20b9182b290958b9e0a6bb3109c81 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 16:53:24 +0200 Subject: [PATCH 18/35] Moved list sorted to conftest. --- tests/conftest.py | 9 ++++++++- tests/test_ulid.py | 9 +-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1552a0c..7a5e8c2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,4 +6,11 @@ def utcnow() -> datetime: def datetimes_almost_equal(a: datetime, b: datetime) -> None: - assert a.replace(microsecond=0) == b.replace(microsecond=0) \ No newline at end of file + assert a.replace(microsecond=0) == b.replace(microsecond=0) + + +def assert_sorted(seq: list) -> None: + last = seq[0] + for item in seq[1:]: + assert last < item + last = item \ No newline at end of file diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 8f3a885..2eda798 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -18,7 +18,7 @@ from ulid import ULID from ulid.value_provider.abstract_value_provider import AbstractValueProvider -from tests.conftest import datetimes_almost_equal, utcnow +from tests.conftest import assert_sorted, datetimes_almost_equal, utcnow @freeze_time() @@ -70,13 +70,6 @@ def test_same_millisecond_overflow() -> None: ULID() -def assert_sorted(seq: list) -> None: - last = seq[0] - for item in seq[1:]: - assert last < item - last = item - - def test_comparison() -> None: with freeze_time() as frozen_time: ulid1 = ULID() From 3df23fe85f4b5f680c4c99fd4d843ca5f332b6fd Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 17:01:38 +0200 Subject: [PATCH 19/35] Added tests for non monotonic value provider. --- .../test_non_monotonic_value_provider.py | 15 +++++++++++++++ .../non_monotonic_value_provider.py | 1 + 2 files changed, 16 insertions(+) create mode 100644 tests/value_provider/test_non_monotonic_value_provider.py diff --git a/tests/value_provider/test_non_monotonic_value_provider.py b/tests/value_provider/test_non_monotonic_value_provider.py new file mode 100644 index 0000000..347a132 --- /dev/null +++ b/tests/value_provider/test_non_monotonic_value_provider.py @@ -0,0 +1,15 @@ +from ulid import constants +from ulid.value_provider import NonMonotonicValueProvider + + +def test_generate_randomness() -> None: + provider = NonMonotonicValueProvider() + + randomness1: bytes = provider.randomness() + randomness1_as_number = int.from_bytes(randomness1, byteorder="big") + randomness2: bytes = provider.randomness() + randomness2_as_number = int.from_bytes(randomness2, byteorder="big") + + assert len(randomness1) == len(randomness2) == constants.RANDOMNESS_LEN + # Assert that they are not monotonic and not equal. + assert abs(randomness1_as_number - randomness2_as_number) > 1 diff --git a/ulid/value_provider/non_monotonic_value_provider.py b/ulid/value_provider/non_monotonic_value_provider.py index cefc95f..5605a4a 100644 --- a/ulid/value_provider/non_monotonic_value_provider.py +++ b/ulid/value_provider/non_monotonic_value_provider.py @@ -4,6 +4,7 @@ from .abstract_value_provider import AbstractValueProvider + class NonMonotonicValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return os.urandom(constants.RANDOMNESS_LEN) \ No newline at end of file From 24a5ccd154bb888940d2db0e6afab3f74e70a92b Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 17:09:38 +0200 Subject: [PATCH 20/35] Added tests for monotonic value provider. --- .../test_monotinoc_value_provider.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 tests/value_provider/test_monotinoc_value_provider.py diff --git a/tests/value_provider/test_monotinoc_value_provider.py b/tests/value_provider/test_monotinoc_value_provider.py new file mode 100644 index 0000000..a1f7e79 --- /dev/null +++ b/tests/value_provider/test_monotinoc_value_provider.py @@ -0,0 +1,37 @@ +import pytest + +from freezegun import freeze_time + +from ulid import constants +from ulid.value_provider import MonotonicValueProvider + + +def test_generate_randomness_monotinic() -> None: + provider = MonotonicValueProvider() + + randomness1: bytes = provider.randomness() + randomness1_as_number = int.from_bytes(randomness1, byteorder="big") + randomness2: bytes = provider.randomness() + randomness2_as_number = int.from_bytes(randomness2, byteorder="big") + randomness3: bytes = provider.randomness() + randomness3_as_number = int.from_bytes(randomness3, byteorder="big") + + assert len(randomness1) == len(randomness2) == len(randomness3) == constants.RANDOMNESS_LEN + # Assert that they are monotonic and not equal. + assert randomness2_as_number - randomness1_as_number == 1 + assert randomness3_as_number - randomness2_as_number == 1 + assert randomness3_as_number - randomness1_as_number == 2 + + +def test_randomness_exhaustion() -> None: + provider = MonotonicValueProvider() + + # Set the previous randomness to the maximum value. + provider.prev_randomness = constants.MAX_RANDOMNESS + + # Attempting to generate randomness within the + # same millisecond should raise an error. + with freeze_time(): + provider.prev_timestamp = provider.timestamp() + with pytest.raises(ValueError, match="Randomness within same millisecond exhausted"): + provider.randomness() From 628dfba2120efe81fbb5e459ad8efad96ff68c6c Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Thu, 5 Mar 2026 17:10:11 +0200 Subject: [PATCH 21/35] Removed unecessary todo. --- tests/value_provider/test_timestamp_value_base_provider.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/value_provider/test_timestamp_value_base_provider.py b/tests/value_provider/test_timestamp_value_base_provider.py index 1f14cad..7286aee 100644 --- a/tests/value_provider/test_timestamp_value_base_provider.py +++ b/tests/value_provider/test_timestamp_value_base_provider.py @@ -1,4 +1,3 @@ -# TODO: Test the value providers. # TODO: Add docstrings to the value providers. from datetime import datetime, timezone From 15db8010dc3401554a412fdd19edcd87a69fb6c3 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:26:33 +0200 Subject: [PATCH 22/35] Updated docstrings to match the new operation. --- ulid/__init__.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index 9690778..40bdf6d 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -13,6 +13,7 @@ from ulid import base32 from ulid import constants from ulid.value_provider import MonotonicValueProvider as ValueProvider +from ulid.value_provider import AbstractValueProvider if TYPE_CHECKING: # pragma: no cover @@ -35,6 +36,7 @@ def validate_value_type(type_to_validate: type, *types_to_validate_against: type message += " or ".join([t.__name__ for t in types_to_validate_against]) raise TypeError(message) + @functools.total_ordering class ULID: provider = ValueProvider() @@ -56,23 +58,30 @@ class ULID: >>> str(ulid) '01E75PVKXA3GFABX1M1J9NZZNF' + The value provider will be used to generate the randomness part + (and the timestamp part if needed) of the `ULID`. + Args: value (bytes, None): A sequence of 16 bytes representing an encoded ULID. + value_provider (AbstractValueProvider, None): The value provider to use to generate the randomness and timestamp. Raises: ValueError: If the provided value is not a valid encoded ULID. """ - def __init__(self, value: bytes | None = None, value_provider: ValueProvider | None = None) -> None: + def __init__(self, value: bytes | None = None, value_provider: AbstractValueProvider | None = None) -> None: if value is not None and len(value) != constants.BYTES_LEN: raise ValueError("ULID has to be exactly 16 bytes long.") value_provider_to_use = value_provider or self.provider self.bytes: bytes = value or ULID.from_timestamp(value_provider_to_use.timestamp(), value_provider=value_provider_to_use).bytes @classmethod - def from_datetime(cls, value: datetime, value_provider: ValueProvider | None = None) -> Self: + def from_datetime(cls, value: datetime, value_provider: AbstractValueProvider | None = None) -> Self: """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the `ULID` will be set to the corresponding timestamp of the datetime. + The value provider will be used to + generate the randomness part of the + `ULID`. Examples: @@ -85,10 +94,13 @@ def from_datetime(cls, value: datetime, value_provider: ValueProvider | None = N return cls.from_timestamp(value.timestamp(), value_provider=value_provider_to_use) @classmethod - def from_timestamp(cls, value: float, value_provider: ValueProvider | None = None) -> Self: + def from_timestamp(cls, value: float, value_provider: AbstractValueProvider | None = None) -> Self: """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a `float` representing the time in seconds (as it would be returned by :func:`time.time()`) or an `int` in milliseconds. + The value provider will be used to + generate the randomness part of the + `ULID`. Examples: From fd5e773e0bf186f9b05c6a5abc95a7513781e49e Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:33:13 +0200 Subject: [PATCH 23/35] Added ruff to the pyproject. --- pyproject.toml | 1 + uv.lock | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 58159f9..eace04b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ dev-dependencies = [ "hatch>=1.14.1", "freezegun>=1.5", "sphinx>=7.4.7", + "ruff==0.12", ] [tool.hatch.metadata.hooks.fancy-pypi-readme] diff --git a/uv.lock b/uv.lock index 4ef6c2c..c6a1fd2 100644 --- a/uv.lock +++ b/uv.lock @@ -905,6 +905,7 @@ dev = [ { name = "hatch" }, { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, + { name = "ruff" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -918,7 +919,8 @@ provides-extras = ["pydantic"] dev = [ { name = "freezegun", specifier = ">=1.5" }, { name = "hatch", specifier = ">=1.14.1" }, - { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest", specifier = ">=8.4" }, + { name = "ruff", specifier = "==0.12" }, { name = "sphinx", specifier = ">=7.4.7" }, ] @@ -968,6 +970,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] +[[package]] +name = "ruff" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, + { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, + { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, + { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, + { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, + { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, + { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, + { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, + { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, + { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, + { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, + { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, + { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, +] + [[package]] name = "secretstorage" version = "3.3.3" From 357034586bd7064f3013e44289922624cc3c936e Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:34:12 +0200 Subject: [PATCH 24/35] Reformatted the files. --- tests/conftest.py | 2 +- tests/test_ulid.py | 20 ++++++++-- .../test_timestamp_value_base_provider.py | 39 +++++++++++-------- ulid/__init__.py | 34 +++++++++++++--- ulid/value_provider/__init__.py | 2 +- .../monotonic_value_provider.py | 3 +- .../non_monotonic_value_provider.py | 2 +- 7 files changed, 72 insertions(+), 30 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a5e8c2..499625c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,4 +13,4 @@ def assert_sorted(seq: list) -> None: last = seq[0] for item in seq[1:]: assert last < item - last = item \ No newline at end of file + last = item diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 2eda798..ddae8b6 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -265,12 +265,16 @@ class Model(BaseModel): "type": "null", } in model_json_schema["properties"]["ulid"]["anyOf"] + def test_ulid_constructor_support_other_value_provider() -> None: random_part = b"\x00" * 10 datetime = utcnow() timestamp_in_seconds = int(datetime.timestamp()) timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) - ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ulid_bytes: bytes = ( + timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ) + class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part @@ -291,12 +295,16 @@ def timestamp(self, value: float | None = None) -> int: # the generated ULIDs should be the same. assert ulid2 == ulid1 + def test_ulid_from_datetime_support_other_value_provider() -> None: random_part = b"\x00" * 10 datetime = utcnow() timestamp_in_seconds = int(datetime.timestamp()) timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) - ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ulid_bytes: bytes = ( + timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ) + class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part @@ -317,12 +325,16 @@ def timestamp(self, value: float | None = None) -> int: # the generated ULIDs should be the same. assert ulid2 == ulid1 + def test_ulid_from_timestamp_support_other_value_provider() -> None: random_part = b"\x00" * 10 datetime = utcnow() timestamp_in_seconds = int(datetime.timestamp()) timestamp_in_milliseconds = int(timestamp_in_seconds * constants.MILLISECS_IN_SECS) - ulid_bytes: bytes = timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ulid_bytes: bytes = ( + timestamp_in_milliseconds.to_bytes(constants.TIMESTAMP_LEN, byteorder="big") + random_part + ) + class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part @@ -341,4 +353,4 @@ def timestamp(self, value: float | None = None) -> int: assert str(ulid1) == base32.encode(ulid_bytes) # since the same dummy value provider is used, # the generated ULIDs should be the same. - assert ulid2 == ulid1 \ No newline at end of file + assert ulid2 == ulid1 diff --git a/tests/value_provider/test_timestamp_value_base_provider.py b/tests/value_provider/test_timestamp_value_base_provider.py index 7286aee..5207226 100644 --- a/tests/value_provider/test_timestamp_value_base_provider.py +++ b/tests/value_provider/test_timestamp_value_base_provider.py @@ -15,26 +15,29 @@ class TestValueProvider(AbstractValueProvider): def randomness(self) -> bytes: - return b'\x00' * 10 + return b"\x00" * 10 @pytest.mark.parametrize( - "datetime_timestamp", - [ - datetime(2026, 6, 6, 6, 6, 6, 6, tzinfo=timezone.utc), - datetime(2024, 1, 1, tzinfo=timezone.utc), - datetime(2025, 12, 31, 9, 6, 3, tzinfo=timezone.utc), - ], + "datetime_timestamp", + [ + datetime(2026, 6, 6, 6, 6, 6, 6, tzinfo=timezone.utc), + datetime(2024, 1, 1, tzinfo=timezone.utc), + datetime(2025, 12, 31, 9, 6, 3, tzinfo=timezone.utc), + ], ) @pytest.mark.parametrize( "value_provider", [ pytest.param(TestValueProvider(), id="CustomedValueProvider"), pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), - pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") - ] + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider"), + ], ) -def test_timestamp(datetime_timestamp: datetime, value_provider: AbstractValueProvider,) -> None: +def test_timestamp( + datetime_timestamp: datetime, + value_provider: AbstractValueProvider, +) -> None: expected_timestamp = int(datetime_timestamp.timestamp() * constants.MILLISECS_IN_SECS) first_timestamp = value_provider.timestamp(datetime_timestamp.timestamp()) @@ -53,10 +56,12 @@ def test_timestamp(datetime_timestamp: datetime, value_provider: AbstractValuePr [ pytest.param(TestValueProvider(), id="CustomedValueProvider"), pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), - pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") - ] + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider"), + ], ) -def test_timestamp_now(value_provider: AbstractValueProvider,) -> None: +def test_timestamp_now( + value_provider: AbstractValueProvider, +) -> None: with freeze_time() as frozen: expected_first_timestamp = int(utcnow().timestamp() * constants.MILLISECS_IN_SECS) first_timestamp = value_provider.timestamp() @@ -76,9 +81,11 @@ def test_timestamp_now(value_provider: AbstractValueProvider,) -> None: [ pytest.param(TestValueProvider(), id="CustomedValueProvider"), pytest.param(MonotonicValueProvider(), id="MonotonicValueProvider"), - pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider") - ] + pytest.param(NonMonotonicValueProvider(), id="NonMonotonicValueProvider"), + ], ) -def test_max_timestamp(value_provider: AbstractValueProvider,) -> None: +def test_max_timestamp( + value_provider: AbstractValueProvider, +) -> None: with pytest.raises(ValueError, match="Value exceeds maximum possible timestamp"): value_provider.timestamp(constants.MAX_TIMESTAMP + 1) diff --git a/ulid/__init__.py b/ulid/__init__.py index 40bdf6d..87e51cc 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -30,7 +30,10 @@ __version__ = version("python-ulid") -def validate_value_type(type_to_validate: type, *types_to_validate_against: type,) -> None: +def validate_value_type( + type_to_validate: type, + *types_to_validate_against: type, +) -> None: if not isinstance(type_to_validate, types_to_validate_against): message = "Value has to be of type " message += " or ".join([t.__name__ for t in types_to_validate_against]) @@ -69,14 +72,27 @@ class ULID: ValueError: If the provided value is not a valid encoded ULID. """ - def __init__(self, value: bytes | None = None, value_provider: AbstractValueProvider | None = None) -> None: + def __init__( + self, + value: bytes | None = None, + value_provider: AbstractValueProvider | None = None, + ) -> None: if value is not None and len(value) != constants.BYTES_LEN: raise ValueError("ULID has to be exactly 16 bytes long.") value_provider_to_use = value_provider or self.provider - self.bytes: bytes = value or ULID.from_timestamp(value_provider_to_use.timestamp(), value_provider=value_provider_to_use).bytes + self.bytes: bytes = ( + value + or ULID.from_timestamp( + value_provider_to_use.timestamp(), value_provider=value_provider_to_use + ).bytes + ) @classmethod - def from_datetime(cls, value: datetime, value_provider: AbstractValueProvider | None = None) -> Self: + def from_datetime( + cls, + value: datetime, + value_provider: AbstractValueProvider | None = None, + ) -> Self: """Create a new :class:`ULID`-object from a :class:`datetime`. The timestamp part of the `ULID` will be set to the corresponding timestamp of the datetime. The value provider will be used to @@ -94,7 +110,11 @@ def from_datetime(cls, value: datetime, value_provider: AbstractValueProvider | return cls.from_timestamp(value.timestamp(), value_provider=value_provider_to_use) @classmethod - def from_timestamp(cls, value: float, value_provider: AbstractValueProvider | None = None) -> Self: + def from_timestamp( + cls, + value: float, + value_provider: AbstractValueProvider | None = None, + ) -> Self: """Create a new :class:`ULID`-object from a timestamp. The timestamp can be either a `float` representing the time in seconds (as it would be returned by :func:`time.time()`) or an `int` in milliseconds. @@ -110,7 +130,9 @@ def from_timestamp(cls, value: float, value_provider: AbstractValueProvider | No """ validate_value_type(value, int, float) value_provider_to_use = value_provider or cls.provider - timestamp = int.to_bytes(value_provider_to_use.timestamp(value), constants.TIMESTAMP_LEN, "big") + timestamp = int.to_bytes( + value_provider_to_use.timestamp(value), constants.TIMESTAMP_LEN, "big" + ) randomness = value_provider_to_use.randomness() return cls.from_bytes(timestamp + randomness) diff --git a/ulid/value_provider/__init__.py b/ulid/value_provider/__init__.py index 67026b3..6859d69 100644 --- a/ulid/value_provider/__init__.py +++ b/ulid/value_provider/__init__.py @@ -1,3 +1,3 @@ from .abstract_value_provider import AbstractValueProvider from .monotonic_value_provider import MonotonicValueProvider -from .non_monotonic_value_provider import NonMonotonicValueProvider \ No newline at end of file +from .non_monotonic_value_provider import NonMonotonicValueProvider diff --git a/ulid/value_provider/monotonic_value_provider.py b/ulid/value_provider/monotonic_value_provider.py index 4e15ecf..c4cb13f 100644 --- a/ulid/value_provider/monotonic_value_provider.py +++ b/ulid/value_provider/monotonic_value_provider.py @@ -5,6 +5,7 @@ from ulid import constants from ulid.value_provider.abstract_value_provider import AbstractValueProvider + class MonotonicValueProvider(AbstractValueProvider): def __init__(self) -> None: self.lock = Lock() @@ -27,4 +28,4 @@ def randomness(self) -> bytes: def increment_bytes(self, value: bytes) -> bytes: length = len(value) - return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big") \ No newline at end of file + return (int.from_bytes(value, byteorder="big") + 1).to_bytes(length, byteorder="big") diff --git a/ulid/value_provider/non_monotonic_value_provider.py b/ulid/value_provider/non_monotonic_value_provider.py index 5605a4a..04b1bd8 100644 --- a/ulid/value_provider/non_monotonic_value_provider.py +++ b/ulid/value_provider/non_monotonic_value_provider.py @@ -7,4 +7,4 @@ class NonMonotonicValueProvider(AbstractValueProvider): def randomness(self) -> bytes: - return os.urandom(constants.RANDOMNESS_LEN) \ No newline at end of file + return os.urandom(constants.RANDOMNESS_LEN) From b2889c081ee9db22652901ad3856b2822d652146 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:36:50 +0200 Subject: [PATCH 25/35] Ran ruff fixes on the files. --- tests/conftest.py | 3 ++- tests/test_ulid.py | 13 ++++++++++--- .../value_provider/test_monotinoc_value_provider.py | 1 - .../test_timestamp_value_base_provider.py | 4 ++-- ulid/__init__.py | 2 +- ulid/value_provider/__init__.py | 6 +++--- ulid/value_provider/abstract_value_provider.py | 6 ++++-- ulid/value_provider/non_monotonic_value_provider.py | 3 +-- 8 files changed, 23 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 499625c..b210d91 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ -from datetime import datetime, timezone +from datetime import datetime +from datetime import timezone def utcnow() -> datetime: diff --git a/tests/test_ulid.py b/tests/test_ulid.py index ddae8b6..1f7ed67 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -1,11 +1,13 @@ +from __future__ import annotations + import json import time import uuid -from collections.abc import Callable from datetime import datetime from datetime import timedelta from datetime import timezone from typing import Optional +from typing import TYPE_CHECKING from typing import Union import pytest @@ -13,12 +15,17 @@ from pydantic import BaseModel from pydantic import ValidationError +from tests.conftest import assert_sorted +from tests.conftest import datetimes_almost_equal +from tests.conftest import utcnow from ulid import base32 from ulid import constants from ulid import ULID from ulid.value_provider.abstract_value_provider import AbstractValueProvider -from tests.conftest import assert_sorted, datetimes_almost_equal, utcnow + +if TYPE_CHECKING: + from collections.abc import Callable @freeze_time() @@ -222,7 +229,7 @@ def test_pydantic_protocol() -> None: ulid = ULID() class Model(BaseModel): - ulid: Optional[ULID] = None # noqa: FA100 + ulid: Optional[ULID] = None model: Model | None = None for value in [ulid, str(ulid), int(ulid), bytes(ulid)]: diff --git a/tests/value_provider/test_monotinoc_value_provider.py b/tests/value_provider/test_monotinoc_value_provider.py index a1f7e79..8804fd4 100644 --- a/tests/value_provider/test_monotinoc_value_provider.py +++ b/tests/value_provider/test_monotinoc_value_provider.py @@ -1,5 +1,4 @@ import pytest - from freezegun import freeze_time from ulid import constants diff --git a/tests/value_provider/test_timestamp_value_base_provider.py b/tests/value_provider/test_timestamp_value_base_provider.py index 5207226..6ea6e79 100644 --- a/tests/value_provider/test_timestamp_value_base_provider.py +++ b/tests/value_provider/test_timestamp_value_base_provider.py @@ -1,9 +1,9 @@ # TODO: Add docstrings to the value providers. -from datetime import datetime, timezone +from datetime import datetime +from datetime import timezone import pytest - from freezegun import freeze_time from tests.conftest import utcnow diff --git a/ulid/__init__.py b/ulid/__init__.py index 87e51cc..e916757 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -12,8 +12,8 @@ from ulid import base32 from ulid import constants -from ulid.value_provider import MonotonicValueProvider as ValueProvider from ulid.value_provider import AbstractValueProvider +from ulid.value_provider import MonotonicValueProvider as ValueProvider if TYPE_CHECKING: # pragma: no cover diff --git a/ulid/value_provider/__init__.py b/ulid/value_provider/__init__.py index 6859d69..a8a7ae2 100644 --- a/ulid/value_provider/__init__.py +++ b/ulid/value_provider/__init__.py @@ -1,3 +1,3 @@ -from .abstract_value_provider import AbstractValueProvider -from .monotonic_value_provider import MonotonicValueProvider -from .non_monotonic_value_provider import NonMonotonicValueProvider +from ulid.value_provider.abstract_value_provider import AbstractValueProvider +from ulid.value_provider.monotonic_value_provider import MonotonicValueProvider +from ulid.value_provider.non_monotonic_value_provider import NonMonotonicValueProvider diff --git a/ulid/value_provider/abstract_value_provider.py b/ulid/value_provider/abstract_value_provider.py index 8a81ab6..f2037bc 100644 --- a/ulid/value_provider/abstract_value_provider.py +++ b/ulid/value_provider/abstract_value_provider.py @@ -1,6 +1,8 @@ -from abc import ABC, abstractmethod -import os +from __future__ import annotations + import time +from abc import ABC +from abc import abstractmethod from ulid import constants diff --git a/ulid/value_provider/non_monotonic_value_provider.py b/ulid/value_provider/non_monotonic_value_provider.py index 04b1bd8..1b106ae 100644 --- a/ulid/value_provider/non_monotonic_value_provider.py +++ b/ulid/value_provider/non_monotonic_value_provider.py @@ -1,8 +1,7 @@ import os from ulid import constants - -from .abstract_value_provider import AbstractValueProvider +from ulid.value_provider.abstract_value_provider import AbstractValueProvider class NonMonotonicValueProvider(AbstractValueProvider): From 8bba5705170be6e167b7cf8c9f74d070edd7778d Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:40:32 +0200 Subject: [PATCH 26/35] Addef fixes to more linits. --- tests/test_ulid.py | 6 +++--- tests/value_provider/test_monotinoc_value_provider.py | 2 +- ulid/value_provider/__init__.py | 7 +++++++ ulid/value_provider/monotonic_value_provider.py | 1 - 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 1f7ed67..70edfe5 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -286,7 +286,7 @@ class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part - def timestamp(self, value: float | None = None) -> int: + def timestamp(self, value: float | None = None) -> int: # noqa: ARG002 because we overriding but still don't have `typing_extensions`. return timestamp_in_milliseconds ulid1 = ULID(value_provider=DummyValueProvider()) @@ -316,7 +316,7 @@ class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part - def timestamp(self, value: float | None = None) -> int: + def timestamp(self, value: float | None = None) -> int: # noqa: ARG002 because we overriding but still don't have `typing_extensions`. return timestamp_in_milliseconds ulid1 = ULID.from_datetime(datetime, value_provider=DummyValueProvider()) @@ -346,7 +346,7 @@ class DummyValueProvider(AbstractValueProvider): def randomness(self) -> bytes: return random_part - def timestamp(self, value: float | None = None) -> int: + def timestamp(self, value: float | None = None) -> int: # noqa: ARG002 because we overriding but still don't have `typing_extensions`. return timestamp_in_milliseconds ulid1 = ULID.from_timestamp(datetime.timestamp(), value_provider=DummyValueProvider()) diff --git a/tests/value_provider/test_monotinoc_value_provider.py b/tests/value_provider/test_monotinoc_value_provider.py index 8804fd4..d7626da 100644 --- a/tests/value_provider/test_monotinoc_value_provider.py +++ b/tests/value_provider/test_monotinoc_value_provider.py @@ -19,7 +19,7 @@ def test_generate_randomness_monotinic() -> None: # Assert that they are monotonic and not equal. assert randomness2_as_number - randomness1_as_number == 1 assert randomness3_as_number - randomness2_as_number == 1 - assert randomness3_as_number - randomness1_as_number == 2 + assert randomness3_as_number - randomness1_as_number == 2 # noqa: PLR2004 Allow use of magic numbers. def test_randomness_exhaustion() -> None: diff --git a/ulid/value_provider/__init__.py b/ulid/value_provider/__init__.py index a8a7ae2..5e00b5f 100644 --- a/ulid/value_provider/__init__.py +++ b/ulid/value_provider/__init__.py @@ -1,3 +1,10 @@ from ulid.value_provider.abstract_value_provider import AbstractValueProvider from ulid.value_provider.monotonic_value_provider import MonotonicValueProvider from ulid.value_provider.non_monotonic_value_provider import NonMonotonicValueProvider + + +__all__ = [ + "AbstractValueProvider", + "MonotonicValueProvider", + "NonMonotonicValueProvider", +] diff --git a/ulid/value_provider/monotonic_value_provider.py b/ulid/value_provider/monotonic_value_provider.py index c4cb13f..f60e1de 100644 --- a/ulid/value_provider/monotonic_value_provider.py +++ b/ulid/value_provider/monotonic_value_provider.py @@ -1,4 +1,3 @@ -import time import os from threading import Lock From bd56789eecc28d3a16d542a2faae59c5d21b2d30 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:46:42 +0200 Subject: [PATCH 27/35] Added docs on the non monotonic stuff. --- README.rst | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7115d57..692cf02 100644 --- a/README.rst +++ b/README.rst @@ -153,7 +153,7 @@ The ``ULID`` class can be directly used for the popular data validation library .. monotonic-begin -Monotonic Support +Monotonic And Non-Monotonic Support ----------------- This library by default supports the implementation for monotonic sort order suggested by the @@ -163,6 +163,39 @@ This means that ULID values generated in the same millisecond will have linear i values. If :math:`r_1` and :math:`r_2` are the randomness values of two ULIDs with the same timestamp, then :math:`r_2 = r_1 + 1`. +You can override this implementation by providing your own value provider to the ``ULID`` +constructor. The library comes with a default monotonic (``MonotonicValueProvider``) +and non-monotonic (``NonMonotonicValueProvider``) value provider that generates +randomness values independently. For example: + +.. code-block:: python + + from ulid import ULID + from ulid.value_provider import NonMonotonicValueProvider + + ulid1 = ULID(value_provider=NonMonotonicValueProvider()) + ulid2 = ULID(value_provider=NonMonotonicValueProvider()) + +You can also implement your own value provider by inheriting from the ``AbstractValueProvider`` and +overriding the ``randomness`` and ``timestamp`` methods. For example: + +.. code-block:: python + + from ulid import ULID + from ulid.value_provider import AbstractValueProvider + + class MyValueProvider(AbstractValueProvider): + def randomness(self) -> bytes: + # Implement your randomness generation logic here + return b"\x00" * 10 # Example: return a fixed randomness value + + def timestamp(self, value: float | None = None) -> int: + # Implement your timestamp generation logic here + return 1772790331000 # Example: return a fixed timestamp value + + ulid1 = ULID(value_provider=MyValueProvider()) + print(ulid1) # Should print "ULID(01KK18KDKR0000000000000000)" + .. monotonic-end .. cli-begin From 2a0c35c4231919afd528f9dbebb4b25ae8374f17 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:48:55 +0200 Subject: [PATCH 28/35] Added docstrings to the abstract value provider. --- ulid/value_provider/abstract_value_provider.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ulid/value_provider/abstract_value_provider.py b/ulid/value_provider/abstract_value_provider.py index f2037bc..156fd07 100644 --- a/ulid/value_provider/abstract_value_provider.py +++ b/ulid/value_provider/abstract_value_provider.py @@ -9,6 +9,11 @@ class AbstractValueProvider(ABC): def timestamp(self, value: float | None = None) -> int: + """ + Generate a timestamp value. + Uses current time in milliseconds if no value is provided, + otherwise converts the provided timestamp in seconds to milliseconds. + """ if value is None: value = time.time_ns() // constants.NANOSECS_IN_MILLISECS elif isinstance(value, float): @@ -19,4 +24,7 @@ def timestamp(self, value: float | None = None) -> int: @abstractmethod def randomness(self) -> bytes: + """ + Generate the randomness value. + """ pass From d69fa5bc827e63b704045773f7aa10f3f5037a11 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:49:51 +0200 Subject: [PATCH 29/35] Removed finished todo. --- tests/value_provider/test_timestamp_value_base_provider.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/value_provider/test_timestamp_value_base_provider.py b/tests/value_provider/test_timestamp_value_base_provider.py index 6ea6e79..b9586e6 100644 --- a/tests/value_provider/test_timestamp_value_base_provider.py +++ b/tests/value_provider/test_timestamp_value_base_provider.py @@ -1,5 +1,3 @@ -# TODO: Add docstrings to the value providers. - from datetime import datetime from datetime import timezone From bc9161392e827d2c46209d3928a1564efc268367 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 11:58:54 +0200 Subject: [PATCH 30/35] Forgot to extent the title line. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 692cf02..1ed809d 100644 --- a/README.rst +++ b/README.rst @@ -154,7 +154,7 @@ The ``ULID`` class can be directly used for the popular data validation library .. monotonic-begin Monotonic And Non-Monotonic Support ------------------ +----------------------------------- This library by default supports the implementation for monotonic sort order suggested by the official ULID specification. From 525a163b312621d6945059f0444bc9429ea26ac2 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 12:00:32 +0200 Subject: [PATCH 31/35] Fixed type check. --- ulid/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ulid/__init__.py b/ulid/__init__.py index e916757..01cb255 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -31,10 +31,10 @@ def validate_value_type( - type_to_validate: type, + value_to_validate: object, *types_to_validate_against: type, ) -> None: - if not isinstance(type_to_validate, types_to_validate_against): + if not isinstance(value_to_validate, types_to_validate_against): message = "Value has to be of type " message += " or ".join([t.__name__ for t in types_to_validate_against]) raise TypeError(message) From 35a9a6f5ca5c9ae7a3444c9456a0fb4630d570ae Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 12:01:10 +0200 Subject: [PATCH 32/35] Removed unecessary pass. --- ulid/value_provider/abstract_value_provider.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ulid/value_provider/abstract_value_provider.py b/ulid/value_provider/abstract_value_provider.py index 156fd07..d238fbd 100644 --- a/ulid/value_provider/abstract_value_provider.py +++ b/ulid/value_provider/abstract_value_provider.py @@ -27,4 +27,3 @@ def randomness(self) -> bytes: """ Generate the randomness value. """ - pass From 09424db9d1096b2212b77d18bce0d4d1f9418091 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 12:02:22 +0200 Subject: [PATCH 33/35] Updated pytest requirment. --- hatch.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hatch.toml b/hatch.toml index 75744be..f05b19f 100644 --- a/hatch.toml +++ b/hatch.toml @@ -11,7 +11,7 @@ config-path = ".ruff_defaults.toml" [envs.hatch-test] extra-dependencies = [ "freezegun==1.5.*", - "pytest==8.4.*", + "pytest>=8.4", ] features = [ "pydantic" From 0643d6b2e9eb7f4f5d88b0536f56dfde9d49247b Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 12:04:55 +0200 Subject: [PATCH 34/35] Removed pytest from extra dependencies. --- hatch.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/hatch.toml b/hatch.toml index f05b19f..e0fd336 100644 --- a/hatch.toml +++ b/hatch.toml @@ -11,7 +11,6 @@ config-path = ".ruff_defaults.toml" [envs.hatch-test] extra-dependencies = [ "freezegun==1.5.*", - "pytest>=8.4", ] features = [ "pydantic" From cee91a9d5cbd14858448cf94570ee3e9df27af41 Mon Sep 17 00:00:00 2001 From: avihais12344 Date: Fri, 6 Mar 2026 12:06:31 +0200 Subject: [PATCH 35/35] Reverted the changes I have done to the pyproject.toml. --- pyproject.toml | 2 -- uv.lock | 98 -------------------------------------------------- 2 files changed, 100 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eace04b..5c3b252 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -107,11 +107,9 @@ max-line-length = 100 [tool.uv] dev-dependencies = [ - "pytest>=8.4", "hatch>=1.14.1", "freezegun>=1.5", "sphinx>=7.4.7", - "ruff==0.12", ] [tool.hatch.metadata.hooks.fancy-pypi-readme] diff --git a/uv.lock b/uv.lock index c6a1fd2..9818362 100644 --- a/uv.lock +++ b/uv.lock @@ -438,31 +438,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] -[[package]] -name = "iniconfig" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - [[package]] name = "jaraco-classes" version = "3.4.0" @@ -835,49 +810,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] -[[package]] -name = "pytest" -version = "8.4.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.10'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.10'" }, - { name = "iniconfig", version = "2.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "packaging", marker = "python_full_version < '3.10'" }, - { name = "pluggy", marker = "python_full_version < '3.10'" }, - { name = "pygments", marker = "python_full_version < '3.10'" }, - { name = "tomli", marker = "python_full_version < '3.10'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", - "python_full_version == '3.10.*'", -] -dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version == '3.10.*'" }, - { name = "iniconfig", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "packaging", marker = "python_full_version >= '3.10'" }, - { name = "pluggy", marker = "python_full_version >= '3.10'" }, - { name = "pygments", marker = "python_full_version >= '3.10'" }, - { name = "tomli", marker = "python_full_version == '3.10.*'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, -] - [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -903,9 +835,6 @@ pydantic = [ dev = [ { name = "freezegun" }, { name = "hatch" }, - { name = "pytest", version = "8.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, - { name = "pytest", version = "9.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "ruff" }, { name = "sphinx", version = "7.4.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, @@ -919,8 +848,6 @@ provides-extras = ["pydantic"] dev = [ { name = "freezegun", specifier = ">=1.5" }, { name = "hatch", specifier = ">=1.14.1" }, - { name = "pytest", specifier = ">=8.4" }, - { name = "ruff", specifier = "==0.12" }, { name = "sphinx", specifier = ">=7.4.7" }, ] @@ -970,31 +897,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742, upload-time = "2025-02-22T07:34:52.422Z" }, ] -[[package]] -name = "ruff" -version = "0.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, -] - [[package]] name = "secretstorage" version = "3.3.3"