From 85835a6f2f1270bdedaf9c8fcda96114305a1f29 Mon Sep 17 00:00:00 2001 From: Martin Domke Date: Fri, 11 Oct 2024 13:01:03 +0200 Subject: [PATCH] feat(ruff): Update linter, rules and fix code accordingly --- .gitignore | 6 +- .pre-commit-config.yaml | 4 +- .ruff_defaults.toml | 547 ++++++++++++++++++++++++++++++++++++++++ CHANGELOG.rst | 27 +- docs/source/conf.py | 3 +- hatch.toml | 41 ++- pyproject.toml | 32 ++- tests/test_ulid.py | 19 +- ulid/__init__.py | 49 ++-- ulid/__main__.py | 27 +- ulid/base32.py | 110 ++++---- ulid/constants.py | 3 + 12 files changed, 722 insertions(+), 146 deletions(-) create mode 100644 .ruff_defaults.toml diff --git a/.gitignore b/.gitignore index 85b4a0e..6575857 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,11 @@ __pycache__ /.cache /.eggs /.mypy_cache +/.pytest_cache +/.ruff_cache +/.devbox + /coverage.xml -/AUTHORS -/ChangeLog /dist/ /build/ /docs/build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 91cabbb..ba65704 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.3.3 + rev: v0.6.9 hooks: - id: ruff - id: ruff-format - repo: https://github.com/pycqa/doc8 - rev: v1.1.1 + rev: v1.1.2 hooks: - id: doc8 diff --git a/.ruff_defaults.toml b/.ruff_defaults.toml new file mode 100644 index 0000000..9428bd2 --- /dev/null +++ b/.ruff_defaults.toml @@ -0,0 +1,547 @@ +line-length = 120 + +[format] +docstring-code-format = true +docstring-code-line-length = 80 + +[lint] +select = [ + "A001", + "A002", + "A003", + "ARG001", + "ARG002", + "ARG003", + "ARG004", + "ARG005", + "ASYNC100", + "B002", + "B003", + "B004", + "B005", + "B006", + "B007", + "B008", + "B009", + "B010", + "B011", + "B012", + "B013", + "B014", + "B015", + "B016", + "B017", + "B018", + "B019", + "B020", + "B021", + "B022", + "B023", + "B024", + "B025", + "B026", + "B028", + "B029", + "B030", + "B031", + "B032", + "B033", + "B034", + "B035", + "B904", + "B905", + "BLE001", + "C400", + "C401", + "C402", + "C403", + "C404", + "C405", + "C406", + "C408", + "C409", + "C410", + "C411", + "C413", + "C414", + "C415", + "C416", + "C417", + "C418", + "C419", + "COM818", + "DTZ001", + "DTZ002", + "DTZ003", + "DTZ004", + "DTZ005", + "DTZ006", + "DTZ007", + "DTZ011", + "DTZ012", + "E101", + "E401", + "E402", + "E701", + "E702", + "E703", + "E711", + "E712", + "E713", + "E714", + "E721", + "E722", + "E731", + "E741", + "E742", + "E743", + "E902", + "EM101", + "EM102", + "EM103", + "EXE001", + "EXE002", + "EXE003", + "EXE004", + "EXE005", + "F401", + "F402", + "F403", + "F404", + "F405", + "F406", + "F407", + "F501", + "F502", + "F503", + "F504", + "F505", + "F506", + "F507", + "F508", + "F509", + "F521", + "F522", + "F523", + "F524", + "F525", + "F541", + "F601", + "F602", + "F621", + "F622", + "F631", + "F632", + "F633", + "F634", + "F701", + "F702", + "F704", + "F706", + "F707", + "F722", + "F811", + "F821", + "F822", + "F823", + "F841", + "F842", + "F901", + "FA100", + "FA102", + "FBT001", + "FBT002", + "FLY002", + "G001", + "G002", + "G003", + "G004", + "G010", + "G101", + "G201", + "G202", + "I001", + "I002", + "ICN001", + "ICN002", + "ICN003", + "INP001", + "INT001", + "INT002", + "INT003", + "ISC003", + "LOG001", + "LOG002", + "LOG007", + "LOG009", + "N801", + "N802", + "N803", + "N804", + "N805", + "N806", + "N807", + "N811", + "N812", + "N813", + "N814", + "N815", + "N816", + "N817", + "N818", + "N999", + "PERF101", + "PERF102", + "PERF401", + "PERF402", + "PGH005", + "PIE790", + "PIE794", + "PIE796", + "PIE800", + "PIE804", + "PIE807", + "PIE808", + "PIE810", + "PLC0105", + "PLC0131", + "PLC0132", + "PLC0205", + "PLC0208", + "PLC0414", + "PLC3002", + "PLE0100", + "PLE0101", + "PLE0116", + "PLE0117", + "PLE0118", + "PLE0237", + "PLE0241", + "PLE0302", + "PLE0307", + "PLE0604", + "PLE0605", + "PLE1142", + "PLE1205", + "PLE1206", + "PLE1300", + "PLE1307", + "PLE1310", + "PLE1507", + "PLE1700", + "PLE2502", + "PLE2510", + "PLE2512", + "PLE2513", + "PLE2514", + "PLE2515", + "PLR0124", + "PLR0133", + "PLR0206", + "PLR0402", + "PLR1711", + "PLR1714", + "PLR1722", + "PLR2004", + "PLR5501", + "PLW0120", + "PLW0127", + "PLW0129", + "PLW0131", + "PLW0406", + "PLW0602", + "PLW0603", + "PLW0711", + "PLW1508", + "PLW1509", + "PLW1510", + "PLW2901", + "PLW3301", + "PT001", + "PT002", + "PT003", + "PT006", + "PT007", + "PT008", + "PT009", + "PT010", + "PT011", + "PT012", + "PT013", + "PT014", + "PT015", + "PT016", + "PT017", + "PT018", + "PT019", + "PT020", + "PT021", + "PT022", + "PT023", + "PT024", + "PT025", + "PT026", + "PT027", + "PYI001", + "PYI002", + "PYI003", + "PYI004", + "PYI005", + "PYI006", + "PYI007", + "PYI008", + "PYI009", + "PYI010", + "PYI011", + "PYI012", + "PYI013", + "PYI014", + "PYI015", + "PYI016", + "PYI017", + "PYI018", + "PYI019", + "PYI020", + "PYI021", + "PYI024", + "PYI025", + "PYI026", + "PYI029", + "PYI030", + "PYI032", + "PYI033", + "PYI034", + "PYI035", + "PYI036", + "PYI041", + "PYI042", + "PYI043", + "PYI044", + "PYI045", + "PYI046", + "PYI047", + "PYI048", + "PYI049", + "PYI050", + "PYI051", + "PYI052", + "PYI053", + "PYI054", + "PYI055", + "PYI056", + "PYI058", + "RET503", + "RET504", + "RET505", + "RET506", + "RET507", + "RET508", + "RSE102", + "RUF001", + "RUF002", + "RUF003", + "RUF005", + "RUF006", + "RUF007", + "RUF008", + "RUF009", + "RUF010", + "RUF012", + "RUF013", + "RUF015", + "RUF016", + "RUF017", + "RUF018", + "RUF019", + "RUF020", + "RUF100", + "S101", + "S102", + "S103", + "S104", + "S105", + "S106", + "S107", + "S108", + "S110", + "S112", + "S113", + "S201", + "S202", + "S301", + "S302", + "S303", + "S304", + "S305", + "S306", + "S307", + "S308", + "S310", + "S311", + "S312", + "S313", + "S314", + "S315", + "S316", + "S317", + "S318", + "S319", + "S320", + "S321", + "S323", + "S324", + "S501", + "S502", + "S503", + "S504", + "S505", + "S506", + "S507", + "S508", + "S509", + "S601", + "S602", + "S604", + "S605", + "S606", + "S607", + "S608", + "S609", + "S611", + "S612", + "S701", + "S702", + "SIM101", + "SIM102", + "SIM103", + "SIM105", + "SIM107", + "SIM108", + "SIM109", + "SIM110", + "SIM112", + "SIM113", + "SIM114", + "SIM115", + "SIM116", + "SIM117", + "SIM118", + "SIM201", + "SIM202", + "SIM208", + "SIM210", + "SIM211", + "SIM212", + "SIM220", + "SIM221", + "SIM222", + "SIM223", + "SIM300", + "SIM910", + "SIM911", + "SLF001", + "SLOT000", + "SLOT001", + "SLOT002", + "T100", + "T201", + "T203", + "TCH001", + "TCH002", + "TCH003", + "TCH004", + "TCH005", + "TCH010", + "TD004", + "TD005", + "TD006", + "TD007", + "TID251", + "TID252", + "TID253", + "TRY002", + "TRY003", + "TRY004", + "TRY201", + "TRY300", + "TRY301", + "TRY302", + "TRY400", + "TRY401", + "UP001", + "UP003", + "UP004", + "UP005", + "UP006", + "UP007", + "UP008", + "UP009", + "UP010", + "UP011", + "UP012", + "UP013", + "UP014", + "UP015", + "UP017", + "UP018", + "UP019", + "UP020", + "UP021", + "UP022", + "UP023", + "UP024", + "UP025", + "UP026", + "UP028", + "UP029", + "UP030", + "UP031", + "UP032", + "UP033", + "UP034", + "UP035", + "UP036", + "UP037", + "UP038", + "UP039", + "UP040", + "UP041", + "W291", + "W292", + "W293", + "W505", + "W605", + "YTT101", + "YTT102", + "YTT103", + "YTT201", + "YTT202", + "YTT203", + "YTT204", + "YTT301", + "YTT302", + "YTT303", +] + +[lint.per-file-ignores] +"**/scripts/*" = [ + "INP001", + "T201", +] +"**/tests/**/*" = [ + "PLC1901", + "PLR2004", + "PLR6301", + "S", + "TID252", +] + +[lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[lint.isort] +known-first-party = ["python_ulid"] + +[lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0459754..2487273 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,23 @@ Changelog Versions follow `Semantic Versioning `_ +`3.0.0`_ - 2024-10-11 +--------------------- +Changed +~~~~~~~ +* Raise `TypeError` instead of `ValueError` if constructor is called with value of wrong type. +* Update ``ruff`` linter rules and switch to ``hatch fmt``. + +Added +~~~~~ +* Added :meth:`.ULID.parse`-method, which allows to create a :class:`.ULID`-instance from an + arbitrary supported input value. `@perrotuerto `_. + +Fixed +~~~~~ +* Documentation bug in the example of :meth:`.ULID.milliseconds` `@tsugumi-sys `_. + + `2.7.0`_ - 2024-06-17 --------------------- Changed @@ -22,7 +39,6 @@ Changed `2.5.0`_ - 2024-04-26 --------------------- - Changed ~~~~~~~ * Generate a more accurate JSON schema with Pydantic's ``BaseModel.model_json_schema()``. This @@ -30,7 +46,6 @@ Changed `2.4.0`_ - 2024-04-02 --------------------- - Added ~~~~~ * :class:`.ULID` objects are now properly serialized when used as Pydantic types `@Avihais12344 `_. @@ -38,7 +53,6 @@ Added `2.3.0`_ - 2024-03-21 --------------------- - Added ~~~~~ * :class:`.ULID` objects can now be converted to bytes with ``bytes(ulid)``. @@ -53,7 +67,6 @@ Changed `2.2.0`_ - 2023-09-21 --------------------- - Added ~~~~~ * Added a new flag ``--uuid4`` to the CLI ``show`` command, that converts the provided ``ULID`` @@ -68,7 +81,6 @@ Added `2.1.0`_ - 2023-09-21 --------------------- - Added ~~~~~ * The new method :meth:`.ULID.to_uuid4` can be used to create an RFC 4122 compliant ``UUID`` from @@ -83,7 +95,6 @@ Changed `2.0.0`_ - 2023-09-20 --------------------- - Added ~~~~~ * New command line interface to easily generate and inspect ULIDs from the terminal @@ -114,7 +125,6 @@ Changed `1.1.0`_ - 2022-03-10 --------------------- - Added ~~~~~ * Added support for Python 3.10. @@ -123,7 +133,6 @@ Added `1.0.3`_ - 2021-07-14 --------------------- - Added ~~~~~ * Enable tool based type checking as described in `PEP-0561`_ by adding the ``py.typed`` marker. @@ -135,7 +144,6 @@ Changed `1.0.0`_ - 2020-04-30 --------------------- - Added ~~~~~ * Added type annotations @@ -174,6 +182,7 @@ Changed * The package now has no external dependencies. * The test-coverage has been raised to 100%. +.. _3.0.0: https://github.com/mdomke/python-ulid/compare/2.7.0...3.0.0 .. _2.7.0: https://github.com/mdomke/python-ulid/compare/2.6.0...2.7.0 .. _2.6.0: https://github.com/mdomke/python-ulid/compare/2.5.0...2.6.0 .. _2.5.0: https://github.com/mdomke/python-ulid/compare/2.4.0...2.5.0 diff --git a/docs/source/conf.py b/docs/source/conf.py index 994c5c0..2be8ac8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,6 +1,7 @@ import os import sys from datetime import datetime +from datetime import timezone sys.path.insert(0, os.path.abspath("../..")) @@ -8,7 +9,7 @@ import ulid -copyright = f"{datetime.now().year}, Martin Domke" +copyright = f"{datetime.now(timezone.utc).year}, Martin Domke" author = "Martin Domke" master_doc = "index" source_suffix = [".rst", ".md"] diff --git a/hatch.toml b/hatch.toml index 0cc8912..b81677e 100644 --- a/hatch.toml +++ b/hatch.toml @@ -1,32 +1,29 @@ [envs.default] -dependencies = [ - "freezegun==1.4.*", - "pytest-cov==4.1.*", - "pytest==8.1.*", -] +installer = "uv" features = [ "pydantic" ] -[envs.default.scripts] -test = "pytest {args:.}" -cov-test = "pytest --cov {args:ulid} --cov-report=term-missing --cov-report=xml" +[envs.hatch-static-analysis] +dependencies = ["ruff==0.6.*"] +config-path = ".ruff_defaults.toml" -[envs.lint] -dependencies = [ - "ruff==0.3.*", - "mypy==1.9.*", - "doc8==1.1.*", +[envs.hatch-test] +extra-dependencies = [ + "freezegun==1.5.*", +] +features = [ + "pydantic" ] -[envs.lint.scripts] -typing = "mypy --install-types --non-interactive {args:ulid}" -style = [ - "ruff format --check --diff {args:.}", - "ruff check {args:.}", +[envs.types] +extra-dependencies = [ + "mypy==1.11.*", ] -fmt = [ - "ruff format {args:.}", - "ruff check --fix {args:.}", +scripts = { check = "mypy --install-types --non-interactive {args:ulid}" } + +[envs.docs] +extra-dependencies = [ + "doc8==1.1.*", ] -docs = "doc8 docs" +scripts = { check = "doc8 docs" } diff --git a/pyproject.toml b/pyproject.toml index 4633082..73e4c05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,20 +50,29 @@ packages = [ "ulid", ] -[tool.black] -line-length = 100 - [tool.ruff] +extend = ".ruff_defaults.toml" target-version = "py39" line-length = 100 +[tool.ruff.format] +preview = true + [tool.ruff.lint] -select = ["A", "B", "C", "C4", "E", "F", "I", "N", "PT", "Q", "RUF", "S", "SIM", "T10", "UP", "W", "YTT"] -fixable = ["RUF100", "I001"] +extend-fixable = ["RUF100", "I001"] ignore = [ "S101", # Allow usage of asserts "A001", # Allow shadowing bultins "A003", # Allow shadowing bultins on classes + "EM101", # Allow raw strings in exceptions + "EM102", # Allow f-strings in exceptions + "EM102", # Allow f-strings in exceptions + "TRY003", # Allow "long" messages in exceptions +] + +[tool.ruff.lint.per-file-ignores] +"docs/**/*" = [ + "INP001", # Ignore possible static credentials ] [tool.ruff.lint.mccabe] @@ -74,6 +83,19 @@ force-single-line = true lines-after-imports = 2 order-by-type = false +[tool.mypy] +disallow_untyped_defs = true +follow_imports = "normal" +ignore_missing_imports = true +pretty = true +show_column_numbers = true +show_error_codes = true +warn_no_return = false +warn_unused_ignores = true +plugins = [ + "pydantic.mypy", +] + [tool.coverage.run] branch = true parallel = true diff --git a/tests/test_ulid.py b/tests/test_ulid.py index 17c02d6..9a1c5f7 100644 --- a/tests/test_ulid.py +++ b/tests/test_ulid.py @@ -1,11 +1,12 @@ +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 @@ -18,6 +19,10 @@ from ulid import ULID +if TYPE_CHECKING: # pragma: no cover + from collections.abc import Callable + + def utcnow() -> datetime: return datetime.now(timezone.utc) @@ -71,7 +76,7 @@ def assert_sorted(seq: list) -> None: def test_comparison() -> None: with freeze_time() as frozen_time: ulid1 = ULID() - assert ulid1 == ulid1 + assert ulid1 == ulid1 # noqa: PLR0124 assert ulid1 == int(ulid1) assert ulid1 == ulid1.bytes assert ulid1 == str(ulid1) @@ -115,7 +120,7 @@ def test_idempotency() -> None: def test_to_uuid4() -> None: ulid = ULID() uuid = ulid.to_uuid4() - assert uuid.version == 4 + assert uuid.version == 4 # noqa: PLR2004 def test_hash() -> None: @@ -215,7 +220,7 @@ def test_pydantic_protocol() -> None: ulid = ULID() class Model(BaseModel): - ulid: Optional[ULID] = None + ulid: ULID | None = None for value in [ulid, str(ulid), int(ulid), bytes(ulid)]: model = Model(ulid=value) @@ -254,6 +259,4 @@ class Model(BaseModel): } in model_json_schema["properties"]["ulid"]["anyOf"] assert { "type": "null", - } in model_json_schema["properties"][ - "ulid" - ]["anyOf"] + } in model_json_schema["properties"]["ulid"]["anyOf"] diff --git a/ulid/__init__.py b/ulid/__init__.py index 3671339..cdf1c7d 100644 --- a/ulid/__init__.py +++ b/ulid/__init__.py @@ -4,10 +4,10 @@ import os import time import uuid -from collections.abc import Callable from datetime import datetime from datetime import timezone from typing import Any +from typing import cast from typing import Generic from typing import TYPE_CHECKING from typing import TypeVar @@ -17,6 +17,8 @@ if TYPE_CHECKING: # pragma: no cover + from collections.abc import Callable + from pydantic import GetCoreSchemaHandler from pydantic import ValidatorFunctionWrapHandler from pydantic_core import CoreSchema @@ -43,7 +45,7 @@ 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 ValueError(message) + raise TypeError(message) return func(cls, value) return wrapped @@ -101,7 +103,7 @@ def from_datetime(cls: type[U], value: datetime) -> U: @classmethod @validate_type(int, float) - def from_timestamp(cls: type[U], value: int | float) -> U: + def from_timestamp(cls: type[U], value: float) -> U: """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. @@ -164,23 +166,20 @@ def parse(cls: type[U], value: Any) -> U: a value when they're unsure what format/primitive type it will be given in. """ if isinstance(value, ULID): - return value + return cast(U, value) if isinstance(value, uuid.UUID): return cls.from_uuid(value) if isinstance(value, str): len_value = len(value) - if len_value in [36, 32]: + if len_value in {constants.UUID_REPR_LEN, constants.UUID_LEN}: return cls.from_uuid(uuid.UUID(value)) - if len_value == 26: + if len_value == constants.REPR_LEN: return cls.from_str(value) - if len_value == 16: + if len_value == constants.BYTES_LEN: return cls.from_hex(value) raise ValueError(f"Cannot parse ULID from string of length {len_value}") if isinstance(value, int): - if len(str(value)) == 13: - return cls.from_timestamp(value) - else: - return cls.from_int(value) + return cls.from_int(value) if isinstance(value, float): return cls.from_timestamp(value) if isinstance(value, datetime): @@ -265,22 +264,22 @@ def __bytes__(self) -> bytes: def __lt__(self, other: Any) -> bool: if isinstance(other, ULID): return self.bytes < other.bytes - elif isinstance(other, int): + if isinstance(other, int): return int(self) < other - elif isinstance(other, bytes): + if isinstance(other, bytes): return self.bytes < other - elif isinstance(other, str): + if isinstance(other, str): return str(self) < other return NotImplemented - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: if isinstance(other, ULID): return self.bytes == other.bytes - elif isinstance(other, int): + if isinstance(other, int): return int(self) == other - elif isinstance(other, bytes): + if isinstance(other, bytes): return self.bytes == other - elif isinstance(other, str): + if isinstance(other, str): return str(self) == other return NotImplemented @@ -293,14 +292,12 @@ def __get_pydantic_core_schema__(cls, source: Any, handler: GetCoreSchemaHandler return core_schema.no_info_wrap_validator_function( cls._pydantic_validate, - core_schema.union_schema( - [ - core_schema.is_instance_schema(ULID), - core_schema.no_info_plain_validator_function(ULID), - core_schema.str_schema(pattern=r"[A-Z0-9]{26}", min_length=26, max_length=26), - core_schema.bytes_schema(min_length=16, max_length=16), - ] - ), + core_schema.union_schema([ + core_schema.is_instance_schema(ULID), + core_schema.no_info_plain_validator_function(ULID), + core_schema.str_schema(pattern=r"[A-Z0-9]{26}", min_length=26, max_length=26), + core_schema.bytes_schema(min_length=16, max_length=16), + ]), serialization=core_schema.to_string_ser_schema( when_used="json-unless-none", ), diff --git a/ulid/__main__.py b/ulid/__main__.py index 0efc815..8e6ba57 100644 --- a/ulid/__main__.py +++ b/ulid/__main__.py @@ -4,17 +4,21 @@ import shutil import sys import textwrap -from collections.abc import Callable -from collections.abc import Sequence from datetime import datetime from functools import partial from typing import Any +from typing import TYPE_CHECKING from uuid import UUID import ulid from ulid import ULID +if TYPE_CHECKING: + from collections.abc import Callable + from collections.abc import Sequence + + def make_parser(prog: str | None = None) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog=prog, @@ -135,30 +139,29 @@ def show(args: argparse.Namespace) -> str: ulid: ULID = ULID.from_str(from_value_or_stdin(args.ulid)) if args.uuid: return str(ulid.to_uuid()) - elif args.uuid4: + if args.uuid4: return str(ulid.to_uuid4()) - elif args.hex: + if args.hex: return ulid.hex - elif args.int: + if args.int: return str(int(ulid)) - elif args.timestamp: + if args.timestamp: return str(ulid.timestamp) - elif args.datetime: + if args.datetime: return ulid.datetime.isoformat() - else: - return textwrap.dedent( - f""" + return textwrap.dedent( + f""" ULID: {ulid!s} Hex: {ulid.hex} Int: {int(ulid)} Timestamp: {ulid.timestamp} Datetime: {ulid.datetime.isoformat()} """ - ).strip() + ).strip() def entrypoint() -> None: # pragma: no cover - print(main(sys.argv[1:])) + pass if __name__ == "__main__": # pragma: no cover diff --git a/ulid/base32.py b/ulid/base32.py index 2c05887..fb94031 100644 --- a/ulid/base32.py +++ b/ulid/base32.py @@ -153,46 +153,42 @@ def encode_timestamp(binary: bytes) -> str: if len(binary) != constants.TIMESTAMP_LEN: raise ValueError("Timestamp value has to be exactly 6 bytes long.") lut = ENCODE - return "".join( - [ - lut[(binary[0] & 224) >> 5], - lut[(binary[0] & 31)], - lut[(binary[1] & 248) >> 3], - lut[((binary[1] & 7) << 2) | ((binary[2] & 192) >> 6)], - lut[((binary[2] & 62) >> 1)], - lut[((binary[2] & 1) << 4) | ((binary[3] & 240) >> 4)], - lut[((binary[3] & 15) << 1) | ((binary[4] & 128) >> 7)], - lut[(binary[4] & 124) >> 2], - lut[((binary[4] & 3) << 3) | ((binary[5] & 224) >> 5)], - lut[(binary[5] & 31)], - ] - ) + return "".join([ + lut[(binary[0] & 224) >> 5], + lut[(binary[0] & 31)], + lut[(binary[1] & 248) >> 3], + lut[((binary[1] & 7) << 2) | ((binary[2] & 192) >> 6)], + lut[((binary[2] & 62) >> 1)], + lut[((binary[2] & 1) << 4) | ((binary[3] & 240) >> 4)], + lut[((binary[3] & 15) << 1) | ((binary[4] & 128) >> 7)], + lut[(binary[4] & 124) >> 2], + lut[((binary[4] & 3) << 3) | ((binary[5] & 224) >> 5)], + lut[(binary[5] & 31)], + ]) def encode_randomness(binary: bytes) -> str: if len(binary) != constants.RANDOMNESS_LEN: raise ValueError("Randomness value has to be exactly 10 bytes long.") lut = ENCODE - return "".join( - [ - lut[(binary[0] & 248) >> 3], - lut[((binary[0] & 7) << 2) | ((binary[1] & 192) >> 6)], - lut[(binary[1] & 62) >> 1], - lut[((binary[1] & 1) << 4) | ((binary[2] & 240) >> 4)], - lut[((binary[2] & 15) << 1) | ((binary[3] & 128) >> 7)], - lut[(binary[3] & 124) >> 2], - lut[((binary[3] & 3) << 3) | ((binary[4] & 224) >> 5)], - lut[(binary[4] & 31)], - lut[(binary[5] & 248) >> 3], - lut[((binary[5] & 7) << 2) | ((binary[6] & 192) >> 6)], - lut[(binary[6] & 62) >> 1], - lut[((binary[6] & 1) << 4) | ((binary[7] & 240) >> 4)], - lut[((binary[7] & 15) << 1) | ((binary[8] & 128) >> 7)], - lut[(binary[8] & 124) >> 2], - lut[((binary[8] & 3) << 3) | ((binary[9] & 224) >> 5)], - lut[(binary[9] & 31)], - ] - ) + return "".join([ + lut[(binary[0] & 248) >> 3], + lut[((binary[0] & 7) << 2) | ((binary[1] & 192) >> 6)], + lut[(binary[1] & 62) >> 1], + lut[((binary[1] & 1) << 4) | ((binary[2] & 240) >> 4)], + lut[((binary[2] & 15) << 1) | ((binary[3] & 128) >> 7)], + lut[(binary[3] & 124) >> 2], + lut[((binary[3] & 3) << 3) | ((binary[4] & 224) >> 5)], + lut[(binary[4] & 31)], + lut[(binary[5] & 248) >> 3], + lut[((binary[5] & 7) << 2) | ((binary[6] & 192) >> 6)], + lut[(binary[6] & 62) >> 1], + lut[((binary[6] & 1) << 4) | ((binary[7] & 240) >> 4)], + lut[((binary[7] & 15) << 1) | ((binary[8] & 128) >> 7)], + lut[(binary[8] & 124) >> 2], + lut[((binary[8] & 3) << 3) | ((binary[9] & 224) >> 5)], + lut[(binary[9] & 31)], + ]) def decode(encoded: str) -> bytes: @@ -211,18 +207,16 @@ def decode_timestamp(encoded: str) -> bytes: lut = DECODE values: bytes = bytes(encoded, "ascii") # https://github.com/ulid/spec?tab=readme-ov-file#overflow-errors-when-parsing-base32-strings - if lut[values[0]] > 7: + if lut[values[0]] > 7: # noqa: PLR2004 raise ValueError(f"Timestamp value {encoded} is too large and will overflow 128-bits.") - return bytes( - [ - ((lut[values[0]] << 5) | lut[values[1]]) & 0xFF, - ((lut[values[2]] << 3) | (lut[values[3]] >> 2)) & 0xFF, - ((lut[values[3]] << 6) | (lut[values[4]] << 1) | (lut[values[5]] >> 4)) & 0xFF, - ((lut[values[5]] << 4) | (lut[values[6]] >> 1)) & 0xFF, - ((lut[values[6]] << 7) | (lut[values[7]] << 2) | (lut[values[8]] >> 3)) & 0xFF, - ((lut[values[8]] << 5) | (lut[values[9]])) & 0xFF, - ] - ) + return bytes([ + ((lut[values[0]] << 5) | lut[values[1]]) & 0xFF, + ((lut[values[2]] << 3) | (lut[values[3]] >> 2)) & 0xFF, + ((lut[values[3]] << 6) | (lut[values[4]] << 1) | (lut[values[5]] >> 4)) & 0xFF, + ((lut[values[5]] << 4) | (lut[values[6]] >> 1)) & 0xFF, + ((lut[values[6]] << 7) | (lut[values[7]] << 2) | (lut[values[8]] >> 3)) & 0xFF, + ((lut[values[8]] << 5) | (lut[values[9]])) & 0xFF, + ]) def decode_randomness(encoded: str) -> bytes: @@ -230,17 +224,15 @@ def decode_randomness(encoded: str) -> bytes: raise ValueError("ULID randomness has to be exactly 16 characters long.") lut = DECODE values = bytes(encoded, "ascii") - return bytes( - [ - ((lut[values[0]] << 3) | (lut[values[1]] >> 2)) & 0xFF, - ((lut[values[1]] << 6) | (lut[values[2]] << 1) | (lut[values[3]] >> 4)) & 0xFF, - ((lut[values[3]] << 4) | (lut[values[4]] >> 1)) & 0xFF, - ((lut[values[4]] << 7) | (lut[values[5]] << 2) | (lut[values[6]] >> 3)) & 0xFF, - ((lut[values[6]] << 5) | (lut[values[7]])) & 0xFF, - ((lut[values[8]] << 3) | (lut[values[9]] >> 2)) & 0xFF, - ((lut[values[9]] << 6) | (lut[values[10]] << 1) | (lut[values[11]] >> 4)) & 0xFF, - ((lut[values[11]] << 4) | (lut[values[12]] >> 1)) & 0xFF, - ((lut[values[12]] << 7) | (lut[values[13]] << 2) | (lut[values[14]] >> 3)) & 0xFF, - ((lut[values[14]] << 5) | (lut[values[15]])) & 0xFF, - ] - ) + return bytes([ + ((lut[values[0]] << 3) | (lut[values[1]] >> 2)) & 0xFF, + ((lut[values[1]] << 6) | (lut[values[2]] << 1) | (lut[values[3]] >> 4)) & 0xFF, + ((lut[values[3]] << 4) | (lut[values[4]] >> 1)) & 0xFF, + ((lut[values[4]] << 7) | (lut[values[5]] << 2) | (lut[values[6]] >> 3)) & 0xFF, + ((lut[values[6]] << 5) | (lut[values[7]])) & 0xFF, + ((lut[values[8]] << 3) | (lut[values[9]] >> 2)) & 0xFF, + ((lut[values[9]] << 6) | (lut[values[10]] << 1) | (lut[values[11]] >> 4)) & 0xFF, + ((lut[values[11]] << 4) | (lut[values[12]] >> 1)) & 0xFF, + ((lut[values[12]] << 7) | (lut[values[13]] << 2) | (lut[values[14]] >> 3)) & 0xFF, + ((lut[values[14]] << 5) | (lut[values[15]])) & 0xFF, + ]) diff --git a/ulid/constants.py b/ulid/constants.py index bc182c6..9adfb7e 100644 --- a/ulid/constants.py +++ b/ulid/constants.py @@ -8,3 +8,6 @@ TIMESTAMP_REPR_LEN = 10 RANDOMNESS_REPR_LEN = 16 REPR_LEN = TIMESTAMP_REPR_LEN + RANDOMNESS_REPR_LEN + +UUID_LEN = 32 +UUID_REPR_LEN = 36 # UUID with dash-separated segments