From bd7be785bdf78b0efb5e629468a5bd0d5b3cce47 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Thu, 3 Jul 2025 13:27:12 -0600 Subject: [PATCH] Publish package, v1.0.0 --- .gitignore | 4 ++++ README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 31 +++++++++++++++++++++++++++++++ uuid7.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 116 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 uuid7.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56a5ba1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +dist/ +.* +!.gitignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..0f0e3a9 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# UUIDv7 for Python + +A simple module for generating UUIDv7 that contain creation +timestamps. Another function for extracting the time of an UUID. + +- **Standard compliant**: Follows the final UUIDv7 specification (drafts were different) +- **Pythonic**: Uses stdlib `datetime` and `UUID` facilities rather than bare strings and milliseconds. + +Note: As of writing, Python itself has no UUIDv7 support yet. + +## Installation + +```sh +pip install uuid7 +``` + +Or for your project using [uv](https://docs.astral.sh/uv/): +```sh +uv add uuid7 +``` + +## Usage + +```python +import uuid7 +from datetime import datetime, UTC +from uuid import UUID + +# Create a random UUIDv7 with current timestamp +u = uuid7.create() + +# Create a UUIDv7 with specific timestamp +when = datetime(1970, 1, 1, tzinfo=UTC) +u = uuid7.create(when) + +# Extract timestamp +u = UUID('00000000-0000-7dac-b3e3-ecb571bb3e2f') +timestamp = uuid7.time(u) # 1970-01-01 UTC +``` + +### `create(when: datetime | None = None) -> UUID` + +Create a UUIDv7 with timestamp-based ordering. Uses current time if `when` is not specified. + +### `extract_time(u: UUID) -> datetime` + +Extract the timestamp from a UUIDv7. Raises ValueError if the UUID is not a UUIDv7. + +### `UUID() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4f0a0b4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,31 @@ +[project] +name = "uuid7" +version = "1.0.0" +description = "UUIDv7 generation with timestamp-based ordering for Python and timestamp extraction." +readme = "README.md" +requires-python = ">=3.10" +dependencies = [] +authors = [ + {name = "Leo Vasanko"}, +] +keywords = ["uuid", "uuid7", "timestamp", "ordering", "database", "primary-key"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "License :: Public Domain", + "Programming Language :: Python :: 3", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", +] + +[project.urls] +Homepage = "https://git.zi.fi/leo/uuid7-python" +Repository = "https://github.com/LeoVasanko/uuid7-python" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["uuid7.py"] diff --git a/uuid7.py b/uuid7.py new file mode 100644 index 0000000..fb2ee8a --- /dev/null +++ b/uuid7.py @@ -0,0 +1,32 @@ +import uuid +from datetime import datetime +from datetime import timezone as _tz +from secrets import token_bytes as _token_bytes + +__all__ = ["create", "time"] + + +def create(when: datetime | None = None) -> uuid.UUID: + """Create a UUIDv7 with timestamp-based ordering. + + Args: + when: Timestamp to use. Defaults to current time. + """ + if when is None: + when = datetime.now() + ts = int(when.timestamp() * 1000).to_bytes(6, "big") + rand = bytearray(_token_bytes(10)) + rand[0] = (rand[0] & 0x0F) | 0x70 + rand[2] = (rand[2] & 0x3F) | 0x80 + return uuid.UUID(bytes=ts + rand) + + +def time(u: uuid.UUID) -> datetime: + """Extract the timestamp from a UUIDv7. + + Raises ValueError if the UUID is not a UUIDv7. + """ + if u.version != 7: + raise ValueError("Not a UUIDv7") + ts = int.from_bytes(u.bytes[:6], "big") + return datetime.fromtimestamp(ts / 1000, tz=_tz.utc)