diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 72a4491..ad7ece7 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,6 +13,11 @@ on: tags: - "*" pull_request: + types: + - opened + - reopened + - synchronize + - ready_for_review workflow_dispatch: permissions: diff --git a/.gitignore b/.gitignore index c8f0442..80b81c0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ # Byte-compiled / optimized / DLL files __pycache__/ .pytest_cache/ +.mypy_cache/ +.ruff_cache/ *.py[cod] # C extensions diff --git a/Cargo.lock b/Cargo.lock index e3c32c2..ba93707 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -22,7 +22,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "fastnanoid" -version = "0.1.1" +version = "0.3.0" dependencies = [ "pyo3", "rand", diff --git a/Cargo.toml b/Cargo.toml index ab78d72..b30f79e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fastnanoid" -version = "0.2.2" +version = "0.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/README.md b/README.md index 68dcbfb..6c0dc0d 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,46 @@ It works as a drop in replacement for [py-nanoid](https://github.com/puyuan/py-n It's 2.7x faster than the original. +## When not to use it + +If you need the same amount of entropy as uuid, you may as well use uuid and +base64url encode it: + +```python +import uuid +from fastnanoid import urlid_to_uuid, uuid_to_urlid +# say you have a uuid, maybe from your database: +id_ = uuid.uuid4() # type: uuid.UUID +# you can encoded it in base64url so it displays as a short string: +urlid = uuid_to_urlid(id_) # type: str +# and when you read it back in from the user, you can convert it back to a normal UUID: +decoded_urlid = urlid_to_uuid(urlid) # type: UUID +``` + +This is simpler than using a nanoid which is not compliant with any existing standards. +If you already have a generated UUID (say from a database), +this is _much_ faster than generating a new nanoid. +(If you don't have a UUID, generating one plus encoding it in base64url is about 50% slower than fastnanoid.) + +\* these are very simple helper functions, you can easily implement them +yourself and save a dependency. + ## Contributing ```sh # local env python -m venv .venv source .venv/bin/activate -pip install -r requirements.txt +pip install -r requirements-dev.txt # build and use maturin develop python -c 'import fastnanoid; print(fastnanoid.generate())' # test cargo test +pytest +mypy +ruff check +ruff format --check ``` ## Credits diff --git a/benchmarks/README.md b/benchmarks/README.md index eb16ae5..c52056f 100644 --- a/benchmarks/README.md +++ b/benchmarks/README.md @@ -2,5 +2,17 @@ ```sh maturin develop --release -python Benchmarks/benchmarks.py +python benchmarks/benchmark.py +``` + +Results: + +``` +$ python benchmarks/benchmark.py +Generating 1,000,000 IDs +- nanoid: 2.22s (2.215884291974362e-06 s/id) +- fastnanoid: 0.86s (8.563055000267923e-07 s/id) - 2.59x faster +- uuid (generate + base64encode): 1.28s (1.2837962500052527e-06 s/id) - 1.73x faster +- uuid (generate only): 0.98s (9.750179999973624e-07 s/id) - 2.27x faster +- uuid (base64encode only): 0.26s (2.631903750007041e-07 s/id) - 8.42x faster ``` diff --git a/benchmarks/benchmark.py b/benchmarks/benchmark.py index b8fd086..39d6b1a 100644 --- a/benchmarks/benchmark.py +++ b/benchmarks/benchmark.py @@ -2,13 +2,34 @@ import fastnanoid import nanoid +import uuid if __name__ == "__main__": n = 1_000_000 print(f"Generating {n:,} IDs") + + print("- nanoid: ", end="", flush=True) nanotime = timeit(nanoid.generate, number=n) + print(f"{nanotime:.2f}s ({nanotime/n} s/id)") + + print("- fastnanoid: ", end="", flush=True) fastnanotime = timeit(fastnanoid.generate, number=n) - print(f"nanoid: {nanotime:.2f}s ({nanotime/n} s/id)") - print(f"fastnanoid: {fastnanotime:.2f}s ({fastnanotime/n} s/id)") - print(f"{nanotime/fastnanotime:.2f}x faster") + print( + f"{fastnanotime:.2f}s ({fastnanotime/n} s/id) - {nanotime/fastnanotime:.2f}x faster" + ) + + print("- uuid (generate + base64encode): ", end="", flush=True) + uuidb64time = timeit(lambda: fastnanoid.uuid_to_urlid(uuid.uuid4()), number=n) + print( + f"{uuidb64time:.2f}s ({uuidb64time/n} s/id) - {nanotime/uuidb64time:.2f}x faster" + ) + + print("- uuid (generate only): ", end="", flush=True) + uuidtime = timeit(uuid.uuid4, number=n) + print(f"{uuidtime:.2f}s ({uuidtime/n} s/id) - {nanotime/uuidtime:.2f}x faster") + + print("- uuid (base64encode only): ", end="", flush=True) + _u = uuid.uuid4() + uuidtime = timeit(lambda: fastnanoid.uuid_to_urlid(_u), number=n) + print(f"{uuidtime:.2f}s ({uuidtime/n} s/id) - {nanotime/uuidtime:.2f}x faster") diff --git a/pyproject.toml b/pyproject.toml index 54263c6..c7d07e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,13 +7,11 @@ name = "fastnanoid" description = "A tiny, secure URL-friendly, and fast unique string ID generator for Python, written in Rust." readme = "README.md" authors = [ - {name = "Oliver Lambson", email = "oliverlambson@gmail.com"}, - {name = "Ochir Erkhembayar"}, + { name = "Oliver Lambson", email = "oliverlambson@gmail.com" }, + { name = "Ochir Erkhembayar" }, ] -maintainers = [ - {name = "Oliver Lambson", email = "oliverlambson@gmail.com"}, -] -license = {file = "LICENSE"} +maintainers = [{ name = "Oliver Lambson", email = "oliverlambson@gmail.com" }] +license = { file = "LICENSE" } requires-python = ">=3.8" keywords = ["nanoid"] classifiers = [ @@ -43,3 +41,18 @@ Issues = "https://github.com/oliverlambson/fastnanoid/issues" features = ["pyo3/extension-module"] python-source = "python" module-name = "fastnanoid.fastnanoid" + +[tool.pytest.ini_options] +addopts = "--doctest-modules" +testpaths = ["python", "benchmarks", "tests"] + +[tool.mypy] +files = ["python/**/*.py", "benchmarks/**/*.py", "tests/**/*.py"] + +[tool.ruff] +include = [ + "pyproject.toml", + "python/**/*.py", + "benchmarks/**/*.py", + "tests/**/*.py", +] diff --git a/python/fastnanoid/__init__.py b/python/fastnanoid/__init__.py index c615fcc..9260bc3 100644 --- a/python/fastnanoid/__init__.py +++ b/python/fastnanoid/__init__.py @@ -1,3 +1,4 @@ from fastnanoid.fastnanoid import generate +from fastnanoid.uuid import urlid_to_uuid, uuid_to_urlid -__all__ = ["generate"] +__all__ = ["generate", "urlid_to_uuid", "uuid_to_urlid"] diff --git a/python/fastnanoid/uuid.py b/python/fastnanoid/uuid.py new file mode 100644 index 0000000..4bf8da4 --- /dev/null +++ b/python/fastnanoid/uuid.py @@ -0,0 +1,28 @@ +import base64 +import uuid + + +def uuid_to_urlid(uuid_: uuid.UUID) -> str: + """Convert a UUID to a short URL-safe string. + + >>> import base64 + >>> import uuid + >>> id_ = uuid.UUID('5d98d578-2731-4a4d-b666-70ca16f10aa2') + >>> url_id = uuid_to_urlid(id_) + >>> print(url_id) + XZjVeCcxSk22ZnDKFvEKog + """ + return base64.urlsafe_b64encode(uuid_.bytes).rstrip(b"=").decode("utf-8") + + +def urlid_to_uuid(url: str) -> uuid.UUID: + """Convert a base64url encoded UUID string to a UUID. + + >>> import base64 + >>> import uuid + >>> url_id = 'XZjVeCcxSk22ZnDKFvEKog' + >>> id_ = urlid_to_uuid(url_id) + >>> print(id_) + 5d98d578-2731-4a4d-b666-70ca16f10aa2 + """ + return uuid.UUID(bytes=base64.urlsafe_b64decode(url + "=" * (len(url) % 4))) diff --git a/requirements-dev.txt b/requirements-dev.txt index 007a2cd..cc407a9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,7 @@ -e . maturin nanoid==2.0.0 +pytest +mypy +ruff +types-nanoid diff --git a/tests/test_fastnanoid.py b/tests/test_fastnanoid.py new file mode 100644 index 0000000..d721d94 --- /dev/null +++ b/tests/test_fastnanoid.py @@ -0,0 +1,6 @@ +from fastnanoid import generate + + +def test_generate(): + assert len(generate()) == 21 + assert len(generate(size=10)) == 10