dealcode (Python)
Collision-free, random-looking codes from a counter. Python implementation of the dealcode spec.
Install
pip install dealcode
Requires Python ≥ 3.9. The only dependency is cryptography (PyCA), used for AES.
Quickstart
from dealcode import Dealcode
codec = Dealcode(key="0a1b...64-hex-chars-from-your-secret-manager")
codec.encode(0) # '767a5b' (6 hex chars)
codec.encode(1) # '421163' never collides with any other counter
codec.decode("421163") # 1
(The outputs shown are the real ones for this exact key and default config — with your own key every counter maps to a different, but equally stable, code.)
The key can be raw bytes (16/24/32 bytes are used as-is as an AES key) or any
string/bytes, which are deterministically expanded to an AES-256 key. Generate
one with openssl rand -hex 32 and keep it in your secret manager — the
mapping is stable only while the key (and every other option) stays fixed.
Options
Dealcode(
key, # bytes | str
alphabet="hex", # "dec" | "hex" | "base32" | "crockford" | "base36"
# | "base58" | "base62" | "base64url" | custom string
min_length=6, # codes start at this length...
max_length=None, # ...and grow one char at a time up to this (default: max for 2^63)
domain="", # namespace: same key, unrelated codes per domain
)
coupon = Dealcode(key, "crockford", domain="coupons") # human-friendly, e.g. '7Q4WKZ'
order = Dealcode(key, "dec", min_length=8, domain="orders") # digits only
fixed = Dealcode(key, "hex", min_length=16, max_length=16) # constant-length
decode raises InvalidCodeError for malformed input — wrong length,
characters outside the alphabet, or a value outside the issuable range.
A well-formed code always decodes to some counter, whether or not that
counter was ever issued (inherent to a permutation — see SPEC §7). Treat
decode as parsing, not proof of existence: look the counter up before acting
on it, and note that a one-character typo in a valid code can resolve to a
different valid counter — add rate limiting (and, for human-typed flows, an
existence check or your own check digit). encode raises RangeError
outside [0, codec.capacity), and construction mistakes (bad key, alphabet,
lengths, domain) raise ConfigError. All errors subclass DealcodeError
(a ValueError).
Preset alphabets normalize obvious typing variants before decoding — hex
is case-insensitive, and crockford also folds O → 0 and I/L → 1 —
but normalization never strips anything: separators and whitespace are not
removed. If you display codes grouped (XXXX-XXXX), strip the hyphens or
spaces in your app before calling decode.
Using it with your database
Dealcode does not talk to your database — it only turns a counter into a code. Any source of never-repeating integers works. With PostgreSQL:
CREATE SEQUENCE order_code_seq AS bigint MINVALUE 0 START WITH 0;
CREATE TABLE orders (
id bigint PRIMARY KEY, -- the counter
code text NOT NULL UNIQUE, -- safety net; alerts on config mistakes
...
);
import os
from sqlalchemy import text
from dealcode import Dealcode, InvalidCodeError
codec = Dealcode(key=os.environ["DEALCODE_KEY"], domain="orders")
def create_order(conn) -> str:
n = conn.execute(text("SELECT nextval('order_code_seq')")).scalar_one()
code = codec.encode(n)
conn.execute(
text("INSERT INTO orders (id, code) VALUES (:id, :code)"),
{"id": n, "code": code},
)
return code
def find_order(conn, code: str):
try:
n = codec.decode(code) # malformed codes never reach the DB
except InvalidCodeError:
return None
return conn.execute(text("SELECT * FROM orders WHERE id = :id"), {"id": n}).first()
Sequences never hand out the same number twice (even across concurrent transactions and rollbacks), so codes never collide. Gaps in the sequence are invisible — codes look random anyway.
If the UNIQUE constraint on code ever fires, do not retry: it means the
key/config changed for an existing namespace. Investigate.
Fixed-length cycling mode
For code shapes that must never grow — airline-PNR-style fixed-length codes —
CyclingDealcode (SPEC §11) fills the entire fixed-length space, and when it
is exhausted refills the same space through a different permutation instead
of adding a character:
from dealcode import CyclingDealcode
pnr = CyclingDealcode(key, "crockford", length=6, domain="bookings")
code = pnr.encode(n) # always exactly 6 chars; cycle = n // pnr.capacity
cycle = pnr.cycle_of(n) # which cycle a counter belongs to — store it with the code
n = pnr.decode(code, cycle=3) # the cycle is required context
Configuration mirrors Dealcode (key, alphabet, domain) with a single fixed
length: 2 <= length <= 128 and 100 <= radix**length <= 2**63.
Codes repeat across cycles (the space is being reused — that's the
point), so keep at most one cycle's codes live per uniqueness scope: retire
or expire cycle e before issuing from e+1, index with
UNIQUE(cycle, code) rather than UNIQUE(code), and store each live code's
cycle — decode needs it. Decoding with a wrong (but in-range) cycle is
not an error — it silently returns a different counter; the existence
check on the decoded counter is what catches it.
Thread safety & performance
A Dealcode instance is immutable and thread-safe; create one per namespace at
startup and reuse it. Encoding is ten AES-CBC-MAC rounds — tens of microseconds,
no allocation-heavy paths, O(1) in the counter value.
Running the tests
From the repository root:
pip install -e ./python pytest # or: export PYTHONPATH=python/src
python -m pytest python/tests
The suite covers the official NIST FF1 sample vectors, every shared
cross-language vector in
testvectors/,
and behavioural/edge cases.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dealcode-1.0.0.tar.gz.
File metadata
- Download URL: dealcode-1.0.0.tar.gz
- Upload date:
- Size: 14.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8035e2bf3369f630655491a77f03dc6e5b9b249f6f710a6882f4550d8404873c
|
|
| MD5 |
3c1469713b9469fbd76f079017f600a3
|
|
| BLAKE2b-256 |
917428e49ede116832c3ad17ba5c75cfefdbff0dc96e1581a8c8566d75cb6115
|
Provenance
The following attestation bundles were made for dealcode-1.0.0.tar.gz:
Publisher:
publish-pypi.yml on algorix-hq/dealcode
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dealcode-1.0.0.tar.gz -
Subject digest:
8035e2bf3369f630655491a77f03dc6e5b9b249f6f710a6882f4550d8404873c - Sigstore transparency entry: 2624120681
- Sigstore integration time:
-
Permalink:
algorix-hq/dealcode@80b90f761a43d58ab0b2f3f290b1bb06c5ca108e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/algorix-hq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@80b90f761a43d58ab0b2f3f290b1bb06c5ca108e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file dealcode-1.0.0-py3-none-any.whl.
File metadata
- Download URL: dealcode-1.0.0-py3-none-any.whl
- Upload date:
- Size: 14.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
00c26bc0d15119170dc10e1746c79c1c8cc23177454b6ca19a62e729dbab2ac0
|
|
| MD5 |
17737f3f7405a054f57438f6aa9e9ba7
|
|
| BLAKE2b-256 |
94bd8211833fe81a90d065fc8e33057599abca80cdbe49558dcafb6cd4e1f080
|
Provenance
The following attestation bundles were made for dealcode-1.0.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on algorix-hq/dealcode
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dealcode-1.0.0-py3-none-any.whl -
Subject digest:
00c26bc0d15119170dc10e1746c79c1c8cc23177454b6ca19a62e729dbab2ac0 - Sigstore transparency entry: 2624120790
- Sigstore integration time:
-
Permalink:
algorix-hq/dealcode@80b90f761a43d58ab0b2f3f290b1bb06c5ca108e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/algorix-hq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@80b90f761a43d58ab0b2f3f290b1bb06c5ca108e -
Trigger Event:
workflow_dispatch
-
Statement type: