Identifier
coola.identifier ¶
Provide identifiers: content-derived, time-ordered and human- friendly.
The stable identifiers (generate_stable_content_id,
generate_stable_uuid5) are derived from nested data through
coola.hashing. The others are generated: time-ordered (ULID, UUIDv7,
Snowflake, ObjectId) or checksummed.
Every generator returns a str identifier except
SnowflakeIdGenerator.generate, which returns a plain int (a
Snowflake ID is defined as a 64-bit integer, e.g. for use as a database
BIGINT primary key).
coola.identifier.SnowflakeIdGenerator ¶
Generate Snowflake-style 64-bit identifiers.
An ID packs a 41-bit millisecond timestamp (counted from
2024-01-01), the 10-bit worker_id of the generator and a 12-bit
sequence number into an integer of at most 63 bits. IDs minted with
the current time are strictly increasing within a generator, from
any number of threads. The generator never raises or sleeps because
of the clock: when more than 4,096 IDs are requested in the same
millisecond, or when the clock moves backward, the timestamp is kept
(or advanced by one millisecond) and the sequence goes on, so the
timestamp of the IDs may run ahead of the clock until the clock
catches up.
Create one generator per process (or worker) and share it: the state
is per generator, and there is no module-level generator because the
worker_id can only be chosen by the caller.
Warning
The uniqueness of the IDs across processes comes from the
worker_id, not from random values. Two generators that use
the same worker_id can mint the same ID in the same
millisecond. Assigning the worker numbers is the job of the
caller (a database row, an environment variable, ...).
Note
A generator cannot be used in the child process of a fork
(os.fork, or multiprocessing with the fork start
method): the child would continue the sequence of its parent
with the same worker_id. generate raises a
RuntimeError in the child, which has to create a generator
with its own worker_id.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
worker_id
|
int
|
The worker ID, in |
required |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import SnowflakeIdGenerator
>>> generator = SnowflakeIdGenerator(worker_id=3)
>>> snowflake_id = generator.generate()
>>> isinstance(snowflake_id, int)
True
coola.identifier.SnowflakeIdGenerator.generate ¶
generate(*, timestamp_ms: int | None = None) -> int
Generate a Snowflake-style 64-bit identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamp_ms
|
int | None
|
The Unix timestamp in milliseconds to encode.
If |
None
|
Returns:
| Type | Description |
|---|---|
int
|
A non-negative integer of at most 63 bits. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the generator is used in the child process of a fork. |
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import SnowflakeIdGenerator
>>> generator = SnowflakeIdGenerator(worker_id=3)
>>> snowflake_id = generator.generate()
>>> isinstance(snowflake_id, int)
True
coola.identifier.extract_object_id_timestamp_s ¶
extract_object_id_timestamp_s(object_id: str) -> int
Extract the Unix timestamp encoded in an ObjectId-style identifier.
Inverse of the encoding done by generate_object_id: decodes the
leading 4 bytes of the 24-character hex string back to the Unix
timestamp (in seconds) it was created from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
object_id
|
str
|
The identifier string previously returned by
|
required |
Returns:
| Type | Description |
|---|---|
int
|
The Unix timestamp, in seconds, that was encoded in |
int
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import extract_object_id_timestamp_s, generate_object_id
>>> extract_object_id_timestamp_s(generate_object_id(timestamp_s=1704067200))
1704067200
coola.identifier.extract_snowflake_timestamp_ms ¶
extract_snowflake_timestamp_ms(snowflake_id: int) -> int
Extract the millisecond timestamp encoded in a Snowflake-style identifier.
Inverse of the encoding done by SnowflakeIdGenerator.generate
: shifts out the worker ID and
sequence fields and adds back the fixed epoch that was subtracted
when the ID was minted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snowflake_id
|
int
|
The identifier previously returned by
|
required |
Returns:
| Type | Description |
|---|---|
int
|
The Unix timestamp, in milliseconds, that was encoded in |
int
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import SnowflakeIdGenerator, extract_snowflake_timestamp_ms
>>> snowflake_id = SnowflakeIdGenerator(worker_id=3).generate(timestamp_ms=1704067200000)
>>> extract_snowflake_timestamp_ms(snowflake_id)
1704067200000
coola.identifier.extract_ulid_timestamp_ms ¶
extract_ulid_timestamp_ms(ulid: str) -> int
Extract the millisecond timestamp encoded in a ULID.
Inverse of the encoding done by generate_ulid: decodes the
26-character Crockford Base32 string back to its 128-bit value and
returns the top 48 bits, which is the timestamp generate_ulid
packed in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ulid
|
str
|
The ULID string previously returned by |
required |
Returns:
| Type | Description |
|---|---|
int
|
The Unix timestamp in milliseconds that was encoded in |
int
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import extract_ulid_timestamp_ms, generate_ulid
>>> extract_ulid_timestamp_ms(generate_ulid(timestamp_ms=1704067200000))
1704067200000
coola.identifier.extract_uuid7_timestamp_ms ¶
extract_uuid7_timestamp_ms(uuid7: str) -> int
Extract the millisecond timestamp encoded in a UUIDv7.
Inverse of the encoding done by generate_uuid7: the timestamp
is the top 48 bits of the UUID, unaffected by the version/variant
bits stamped into the lower 80 bits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uuid7
|
str
|
The UUIDv7 string previously returned by
|
required |
Returns:
| Type | Description |
|---|---|
int
|
The Unix timestamp in milliseconds that was encoded in |
int
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import extract_uuid7_timestamp_ms, generate_uuid7
>>> extract_uuid7_timestamp_ms(generate_uuid7(timestamp_ms=1704067200000))
1704067200000
coola.identifier.generate_checksummed_id ¶
generate_checksummed_id(
*,
length: int = _DEFAULT_LENGTH,
group_size: int = _DEFAULT_GROUP_SIZE,
sep: str = "-",
) -> str
Generate a random identifier with a trailing check symbol.
Draws length random Crockford Base32 characters, appends one
check symbol computed from them (a mod-37 checksum, per the
Crockford Base32 spec), and groups the result into chunks of
group_size characters separated by sep for readability.
The check symbol is always a character of the 32-character
alphabet (0-9A-Z without I, L, O and U): when the
checksum of a payload is one of the 5 extra symbols of the
specification (*~$=U), the payload is drawn again (5 payloads in
37, so 1.16 draws on average). The identifier is therefore safe in
a shell, a URL and a spreadsheet, at the cost of 0.21 bit of entropy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
int
|
The number of random (non-check) characters to generate. Must be positive. |
_DEFAULT_LENGTH
|
group_size
|
int
|
The number of characters per group in the
formatted output. Must be positive. Pass a number greater
than or equal to |
_DEFAULT_GROUP_SIZE
|
sep
|
str
|
The separator inserted between groups. Must be empty or
a single character that is not in the Crockford Base32
alphabet (the case is ignored), or |
'-'
|
Returns:
| Type | Description |
|---|---|
str
|
A string of |
str
|
symbol, grouped by |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import generate_checksummed_id, verify_checksummed_id
>>> checksummed_id = generate_checksummed_id()
>>> verify_checksummed_id(checksummed_id)
True
>>> len(generate_checksummed_id(length=8, group_size=3, sep=" "))
11
coola.identifier.generate_object_id ¶
generate_object_id(
*, timestamp_s: int | None = None
) -> str
Generate a MongoDB ObjectId style 12-byte identifier.
An ObjectId packs a 32-bit timestamp in seconds followed by 64 random bits into a 24-character lowercase hex string. Because the timestamp is the most significant part, ObjectIds generated later sort (as plain strings) after ObjectIds generated earlier.
Note
When the current time is used (timestamp_s=None), ObjectIds
are strictly increasing within a process: ObjectIds generated
in the same second (or after the clock moved backward) keep the
timestamp and increment the random part by one. The state is
per process, protected by a lock, and reset in the child
process of a fork. Across processes, ObjectIds generated in the
same second are not ordered. When timestamp_s is given, the
random part is always random and the state is not touched: two
ObjectIds with the same timestamp are not ordered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamp_s
|
int | None
|
The Unix timestamp in seconds to encode. If
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
A 24-character lowercase hex string. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import generate_object_id
>>> object_id = generate_object_id()
>>> len(object_id)
24
coola.identifier.generate_stable_content_id ¶
generate_stable_content_id(
data: object,
*,
registry: HasherRegistry | None = None,
length: int = 64,
ignore_unhashable: bool = False,
) -> str
Compute a content-addressed identifier for a nested data structure.
Unlike generate_stable_uuid5, the returned identifier is the raw
hash_object digest: it is not reshaped into a UUID, so its
collision resistance and length are exactly those of the
underlying hash. Use it when more than the 122 bits of a UUID are
needed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
object
|
The data to derive an identifier from. Can be a nested
structure such as a |
required |
registry
|
HasherRegistry | None
|
The registry used to resolve hashers for each data
type, forwarded to |
None
|
length
|
int
|
The desired length of the returned hex string,
forwarded to |
64
|
ignore_unhashable
|
bool
|
Forwarded to |
False
|
Returns:
| Type | Description |
|---|---|
str
|
A lowercase hex string identifier of the requested |
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
Example
>>> from coola.identifier import generate_stable_content_id
>>> generate_stable_content_id({"source": "cats.txt", "page": 1}) # doctest: +ELLIPSIS
'...'
>>> generate_stable_content_id(
... {"page": 1, "source": "cats.txt"}
... ) == generate_stable_content_id({"source": "cats.txt", "page": 1})
True
coola.identifier.generate_stable_uuid5 ¶
generate_stable_uuid5(
data: object,
*,
registry: HasherRegistry | None = None,
namespace: UUID = _NAMESPACE,
ignore_unhashable: bool = False,
) -> str
Compute a stable, reproducible UUID for a nested data structure.
Hashes data via generate_stable_content_id (at its maximum,
128-hex-digit length) to guarantee a consistent digest regardless of
e.g. mapping insertion order, then derives a deterministic UUID from
that digest using uuid.uuid5 under a fixed namespace.
Note
generate_stable_uuid5(a) == generate_stable_uuid5(b) if and
only if hash_object(a, length=128) == hash_object(b,
length=128) (modulo the astronomically unlikely case of a
uuid.uuid5 collision on two different digests). A UUID has
122 random bits, so its collision resistance is bounded by its
width (birthday bound 2^61), not by SHA-1 or by the strength of
the digest: use generate_stable_content_id when more bits
are needed.
Warning
The value returned by generate_stable_uuid5 for a given
data is stable only as long as hash_object (and the
hashers resolved by registry for the types in data) keep
producing the same digest for that data. A change to the
hashing algorithms of coola in a future release would
silently change the UUIDs produced here. Do not rely on
cross-version stability for UUIDs persisted long-term (e.g. as
database primary keys) unless you pin coola and pass an
explicit, version-controlled registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
object
|
The data to derive a UUID from. Can be a nested structure
such as a |
required |
registry
|
HasherRegistry | None
|
The registry used to resolve hashers for each data
type, forwarded to |
None
|
namespace
|
UUID
|
The UUID namespace passed to |
_NAMESPACE
|
ignore_unhashable
|
bool
|
Forwarded to |
False
|
Returns:
| Type | Description |
|---|---|
str
|
A lowercase UUID string of the form |
str
|
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If |
Example
>>> from coola.identifier import generate_stable_uuid5
>>> generate_stable_uuid5({"source": "cats.txt", "page": 1}) # doctest: +ELLIPSIS
'...'
>>> generate_stable_uuid5({"page": 1, "source": "cats.txt"}) == generate_stable_uuid5(
... {"source": "cats.txt", "page": 1}
... )
True
coola.identifier.generate_ulid ¶
generate_ulid(*, timestamp_ms: int | None = None) -> str
Generate a ULID (Universally Unique Lexicographically Sortable Identifier).
A ULID packs a 48-bit millisecond timestamp followed by 80 bits of
randomness into a 26-character Crockford Base32 string. Because the
timestamp is the most significant part, ULIDs generated later sort
(lexicographically, as plain strings) after ULIDs generated
earlier, unlike uuid.uuid4 which sorts randomly.
Note
When the current time is used (timestamp_ms=None), ULIDs
are monotonic within a process, as recommended by the ULID
specification: ULIDs generated in the same millisecond (or
after the clock moved backward) keep the timestamp and
increment the random part by one, so they still sort in the
order of generation. The state is per process, protected by a
lock, and reset in the child process of a fork. Across
processes, ULIDs generated in the same millisecond are not
ordered. When timestamp_ms is given, the random part is
always random: two ULIDs with the same timestamp are not
ordered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamp_ms
|
int | None
|
The Unix timestamp in milliseconds to encode. If
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
A 26-character uppercase Crockford Base32 ULID string. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import generate_ulid
>>> ulid = generate_ulid()
>>> len(ulid)
26
coola.identifier.generate_uuid7 ¶
generate_uuid7(*, timestamp_ms: int | None = None) -> str
Generate a UUIDv7 (RFC 9562) identifier.
A UUIDv7 packs a 48-bit millisecond timestamp, followed by the
4-bit version, 12 bits of randomness, the 2-bit variant, and 62
more bits of randomness, into a standard 128-bit UUID layout.
Because the timestamp is the most significant part, UUIDv7 values
generated later sort (lexicographically, as canonical UUID
strings) after UUIDv7 values generated earlier, unlike
uuid.uuid4 which sorts randomly.
Note
When the current time is used (timestamp_ms=None), UUIDv7
values are monotonic within a process (the "method 1"
counter of RFC 9562): values generated in the same
millisecond (or after the clock moved backward) keep the
timestamp and increment the 74 random bits by one, so they
still sort in the order of generation, which keeps database
indexes append-only. The state is per process, protected by a
lock, and reset in the child process of a fork. Across
processes, values generated in the same millisecond are not
ordered. When timestamp_ms is given, the random bits are
always random: two values with the same timestamp are not
ordered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timestamp_ms
|
int | None
|
The Unix timestamp in milliseconds to encode. If
|
None
|
Returns:
| Type | Description |
|---|---|
str
|
A lowercase UUID string of the form |
str
|
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import generate_uuid7
>>> uuid7 = generate_uuid7()
>>> len(uuid7)
36
coola.identifier.verify_checksummed_id ¶
verify_checksummed_id(
identifier: str, *, sep: str = "-"
) -> bool
Verify the check symbol of an identifier from
generate_checksummed_id.
Per the Crockford Base32 spec, decoding is case-insensitive and
normalizes the characters most often confused when an identifier is
hand-transcribed: 'O' with '0', and 'I'/'L' with
'1'. identifier is normalized this way before its check
symbol is verified, so e.g. a lowercase retype or an 'O' typed
for a '0' still verifies correctly. The 5 extra check symbols of
the specification (*~$=U) are accepted, so an identifier from
another implementation verifies.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
The identifier to verify, in the grouped form
returned by |
required |
sep
|
str
|
The group separator used when |
'-'
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
the characters preceding it, |
bool
|
when |
bool
|
character or a character outside the extended Crockford Base32 |
bool
|
alphabet, or is too short to contain a payload and a check |
bool
|
symbol). |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If |
Example
>>> from coola.identifier import generate_checksummed_id, verify_checksummed_id
>>> verify_checksummed_id(generate_checksummed_id())
True
>>> verify_checksummed_id("not-a-valid-id")
False
>>> verify_checksummed_id(generate_checksummed_id().lower())
True