Skip to content

Commit

Permalink
feat: uuid recommendation (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliverlambson authored Aug 11, 2024
1 parent 3047556 commit 575f213
Show file tree
Hide file tree
Showing 12 changed files with 134 additions and 14 deletions.
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

0 comments on commit 575f213

Please sign in to comment.