Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion local/tests/test_collision.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

from collections import Counter
from functools import reduce
from math import ceil, floor
Expand Down Expand Up @@ -42,7 +44,7 @@ def _id_to_int(value: str, radix: int = str_base) -> int:

bucket_length: int = ceil(str_base**23 / bucket_count)

for _ in range(0, max_ids):
for _ in range(max_ids):
uid: str = cuid()
result["ids"].add(uid)
result["id_histogram"][_id_to_int(uid[1:]) // bucket_length] += 1
Expand Down
1,523 changes: 873 additions & 650 deletions pdm.lock

Large diffs are not rendered by default.

74 changes: 37 additions & 37 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,46 +35,47 @@ source = "scm"

[tool.pdm.scripts]
# Default to linting the entire src/ directory, but allow overriding with specific files
black = "black {args:src/ local/tests/}"
ruff = "ruff check --fix --exit-zero {args:src/ local/tests/}"
ruff-check = "ruff check --fix --exit-zero {args:src/ local/tests/}"
ruff-format = "ruff format {args:src/ local/tests/}"
spelling = "codespell {args:src/ local/tests/}"
pylint = "pylint {args:src/ local/tests/}"
safety = "safety {args:check --bare}"
typing = "mypy {args:src/ local/tests/}"
lint-fast = {composite = ["black", "ruff"]}
lint-full = {composite = ["lint-fast", "spelling", "pylint", "typing", "safety"]}
update-pip = "pip install --upgrade pip"
ruff-fix = {composite = ["ruff-check", "ruff-format"]}
lint-full = {composite = ["ruff-fix", "spelling", "pylint", "typing", "safety"]}
tox-full = {composite = ["update-pip", "lint-full"]}
testing = "pytest local/tests"
testing-slow = "pytest local/tests --runslow"
tox = "tox --parallel auto"

[tool.pdm.dev-dependencies]
lint = [
"black~=23.3.0", # https://github.com/psf/black (latest: 23.3.0)
"codespell~=2.2.5", # https://github.com/codespell-project/codespell (latest: 2.2.5)
"pylint~=2.17.4", # https://github.com/PyCQA/pylint (latest: 2.17.4)
"requests>=2.31.0", # https://github.com/psf/requests (latest: 2.31.0)
"ruff~=0.0.275", # https://github.com/charliermarsh/ruff (latest: 0.0.275)
"safety==2.4.0b1", # https://github.com/pyupio/safety (latest: 2.3.5)
"codespell~=2.2.6", # https://github.com/codespell-project/codespell (latest: 2.2.6)
"pylint~=3.1.0", # https://github.com/pylint-dev/pylint (latest: 3.1.0)
"requests>=2.31.0", # https://github.com/psf/requests (latest: 2.31.0)
"ruff~=0.3.7", # https://github.com/astral-sh/ruff (latest: 0.3.7)
"safety==3.1.0", # https://github.com/pyupio/safety (latest: 3.1.0)
]
test = [
"pytest~=7.4.0", # https://github.com/pytest-dev/pytest (latest: 7.4.0)
"pytest-mock~=3.11.1", # https://github.com/pytest-dev/pytest-mock/ (latest: 3.11.1)
"pytest-sugar~=0.9.7", # https://github.com/Teemu/pytest-sugar/ (latest: 0.9.7)
"pytest~=8.1.1", # https://github.com/pytest-dev/pytest (latest: 8.1.1)
"pytest-mock~=3.14.0", # https://github.com/pytest-dev/pytest-mock/ (latest: 3.14.0)
"pytest-sugar~=1.0.0", # https://github.com/Teemu/pytest-sugar (latest: 1.0.0)
]
tox = [
# Version reduced to prevent `packaging` conflict with safety
"tox~=4.4.12", # https://github.com/tox-dev/tox (latest: 4.6.3)
"tox-pdm~=0.6.1", # https://github.com/pdm-project/tox-pdm (latest: 0.6.1)
"tox~=4.14.2", # https://github.com/tox-dev/tox (latest: 4.14.2)
"tox-pdm~=0.7.2", # https://github.com/pdm-project/tox-pdm (latest: 0.7.2)
]
typing = [
"mypy~=1.4.1", # https://github.com/python/mypy (latest: 1.4.1)
"mypy~=1.9.0", # https://github.com/python/mypy (latest: 1.9.0)
]

[tool.tox]
legacy_tox_ini = """
[tox]
min_version = 4
env_list = py3{8,9,10,11}, check
env_list = py3{8,9,10,11,12}, check
work_dir = local/.tox
isolated_build = True

Expand All @@ -87,54 +88,53 @@ legacy_tox_ini = """
description = run linters and typing
skip_install = true
groups = lint, typing, test
commands = lint-full
commands = tox-full
"""

[tool.black]
line-length = 120
target_version = ["py38"]

[tool.ruff]
line-length = 120
src = ["src"]
target-version = "py38"
cache-dir = "local/.ruff_cache"
# "E", "F" already included from `select`
# https://beta.ruff.rs/docs/rules
extend-select = [
"W", "C90", "I", "N", "UP", "S", "BLE",
"B", "A", "COM", "C4", "DTZ", "T10", "EM",
"ISC", "ICN", "G", "INP", "PIE", "T20", "PT",
"Q", "RSE", "RET", "SLF", "SIM", "INT", "ARG",
"PTH", "PGH", "PL", "TRY", "RUF", "D", "ANN",
"PYI", "TCH", "ERA",
force-exclude = true

[tool.ruff.lint]
# https://docs.astral.sh/ruff/rules/
select = [
"F", "E", "W", "C90", "I", "N", "ASYNC",
"TRIO", "S", "BLE", "B", "A", "COM", "C4",
"DTZ", "T10", "EM", "FA", "ISC", "ICN",
"G", "INP", "PIE", "T20", "PT", "Q", "RSE",
"RET", "SLF", "SLOT", "SIM", "INT", "ARG",
"PTH", "PGH", "PL", "TRY", "FLY", "PERF",
"LOG", "RUF",
] # purposely not including "DJ", "YTT", "EXE", "PD", "NPY", "FBT" (boolean checking)
extend-ignore = [
ignore = [
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D104", # Missing docstring in public package
"D205", # Blank line required between summary line and description
"D401", # First line should be in imperative mood
"COM812", "ISC001", # Conflict with formatter
]
# Don't automatically remove `print`
# Stop automatically removing unused imports
unfixable = ["T201", "F401", "F841"]
force-exclude = true

[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
# Ignore `assert` in test files (S101), magic values (PLR2004), and private member accessed (SLF001)
"test_*.py" = ["S101", "PLR2004", "SLF001"]

[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["src"]
section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"]

[tool.ruff.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "numpy"

[tool.pytest.ini_options]
minversion = "7.3"
minversion = "8.1"
cache_dir = "local/.pytest_cache"
python_files = "test_*.py"

Expand Down
1 change: 1 addition & 0 deletions src/cuid2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Next generation GUIDs. Collision-resistant ids optimized for horizontal scaling and performance."""

from .generator import DEFAULT_LENGTH, INITIAL_COUNT_MAX, Cuid, cuid_wrapper

__all__ = ["Cuid", "DEFAULT_LENGTH", "INITIAL_COUNT_MAX", "cuid_wrapper"]
15 changes: 8 additions & 7 deletions src/cuid2/generator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import time
from math import floor
from secrets import SystemRandom
Expand All @@ -9,8 +11,7 @@
from _random import Random

class FingerprintCallable(Protocol): # pylint: disable=too-few-public-methods
def __call__(self: "FingerprintCallable", random_generator: Random) -> str:
...
def __call__(self: FingerprintCallable, random_generator: Random) -> str: ...


# ~22k hosts before 50% chance of initial counter collision
Expand All @@ -22,11 +23,11 @@ def __call__(self: "FingerprintCallable", random_generator: Random) -> str:

class Cuid: # pylint: disable=too-few-public-methods
def __init__(
self: "Cuid",
random_generator: Callable[[], "Random"] = SystemRandom,
self: Cuid,
random_generator: Callable[[], Random] = SystemRandom,
counter: Callable[[int], Callable[[], int]] = utils.create_counter,
length: int = DEFAULT_LENGTH,
fingerprint: "FingerprintCallable" = utils.create_fingerprint,
fingerprint: FingerprintCallable = utils.create_fingerprint,
) -> None:
"""Initialization function for the Cuid class that generates a universally unique,
base36 encoded string.
Expand Down Expand Up @@ -55,12 +56,12 @@ def __init__(
msg = "Length must never exceed 98 characters."
raise ValueError(msg)

self._random: "Random" = random_generator()
self._random: Random = random_generator()
self._counter: Callable[[], int] = counter(floor(self._random.random() * INITIAL_COUNT_MAX))
self._length: int = length
self._fingerprint: str = fingerprint(random_generator=self._random)

def generate(self: "Cuid", length: Optional[int] = None) -> str:
def generate(self: Cuid, length: Optional[int] = None) -> str:
"""Generates a universally unique, base36 encoded string with a specified length.

Parameters
Expand Down
10 changes: 6 additions & 4 deletions src/cuid2/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import os
import socket
import string
Expand Down Expand Up @@ -46,7 +48,7 @@ def counter() -> int:
return counter


def create_fingerprint(random_generator: "Random", fingerprint_data: Optional[str] = "") -> str:
def create_fingerprint(random_generator: Random, fingerprint_data: Optional[str] = "") -> str:
"""Creates a fingerprint, by default combining process ID, hostname, and environment variables
with entropy and then hashing the result.

Expand Down Expand Up @@ -76,7 +78,7 @@ def create_fingerprint(random_generator: "Random", fingerprint_data: Optional[st
return create_hash(fingerprint)[0:BIG_LENGTH]


def create_entropy(random_generator: "Random", length: int = 4) -> str:
def create_entropy(random_generator: Random, length: int = 4) -> str:
"""Creates a random string of specified length using a base36 encoding.

Parameters
Expand Down Expand Up @@ -126,14 +128,14 @@ def create_hash(data: str = "") -> str:
Base36 encoding of the SHA-512 hash of the input string `data`, with the first character dropped.

"""
hashed_value: "_Hash" = sha512(data.encode())
hashed_value: _Hash = sha512(data.encode())
hashed_int: int = int.from_bytes(hashed_value.digest(), byteorder="big")

# Drop the first character because it will bias the histogram to the left.
return base36_encode(hashed_int)[1:]


def create_letter(random_generator: "Random") -> str:
def create_letter(random_generator: Random) -> str:
"""Generates a random lowercase letter using a given random number generator.

Parameters
Expand Down