Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: uuid recommendation #3

Merged
merged 1 commit into from
Aug 11, 2024
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
5 changes: 5 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ on:
tags:
- "*"
pull_request:
types:
- opened
- reopened
- synchronize
- ready_for_review
workflow_dispatch:

permissions:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
.mypy_cache/
.ruff_cache/
*.py[cod]

# C extensions
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion benchmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
27 changes: 24 additions & 3 deletions benchmarks/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
25 changes: 19 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "[email protected]"},
{name = "Ochir Erkhembayar"},
{ name = "Oliver Lambson", email = "[email protected]" },
{ name = "Ochir Erkhembayar" },
]
maintainers = [
{name = "Oliver Lambson", email = "[email protected]"},
]
license = {file = "LICENSE"}
maintainers = [{ name = "Oliver Lambson", email = "[email protected]" }]
license = { file = "LICENSE" }
requires-python = ">=3.8"
keywords = ["nanoid"]
classifiers = [
Expand Down Expand Up @@ -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",
]
3 changes: 2 additions & 1 deletion python/fastnanoid/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
28 changes: 28 additions & 0 deletions python/fastnanoid/uuid.py
Original file line number Diff line number Diff line change
@@ -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)))
4 changes: 4 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
-e .
maturin
nanoid==2.0.0
pytest
mypy
ruff
types-nanoid
6 changes: 6 additions & 0 deletions tests/test_fastnanoid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from fastnanoid import generate


def test_generate():
assert len(generate()) == 21
assert len(generate(size=10)) == 10