Identifiers
Entity identification: namespaces, normalization, UUID generation.
Deterministic identifier generation — UUID5 namespaces, name normalization, and attestation UUID helpers.
This module is deliberately generic. It knows nothing about Persons, Committees, Organizations, elect.info, FEC, or any domain concept. Consumers declare their own root seed and per-entity-type sub-namespaces using the factory helpers here.
Typical usage from a consumer project:
from siege_utilities.identifiers import (
derive_root,
derive_sub_namespace,
uuid5_from_seed,
normalize_name_v1,
attestation_uuid,
)
# consumer constants (declared once, hardcoded after first derivation):
ROOT = derive_root("mydomain.example.com")
PERSON_NS = derive_sub_namespace(ROOT, "person")
# generate a deterministic person UUID from a canonical seed:
pid = uuid5_from_seed(PERSON_NS, "FEC_CAND:H4VA07136")
# derive the attestation sub-namespace (like any other sub-namespace):
ATTESTATION_NS = derive_sub_namespace(ROOT, "attestation")
# generate an idempotent attestation UUID:
aid = attestation_uuid(
namespace=ATTESTATION_NS,
source_artifact_hash="abc123",
record_line=42,
parser_version="parser-v1.0.0",
values_hash="deadbeef",
)
- siege_utilities.identifiers.attestation_uuid(*, namespace, source_artifact_hash, record_line, parser_version, values_hash)[source]
Generate a parser-version-aware attestation UUID.
Re-processing the same source line with the same parser version produces the same UUID (write-path idempotency). Bumping the parser version produces a new UUID, so the re-parse is correctly treated as a new attestation rather than silently clobbering the old one.
The seed is built as
RS-delimited components (source_artifact_hash RS record_line RS parser_version RS values_hash), whereRSis ASCII 0x1E (Record Separator). This delimiter cannot appear in hash strings, version tags, or file paths, so there are no component-boundary collisions regardless of component content.This helper is deliberately generic about what an “attestation” is — it just requires a namespace and four stable inputs. Consumers with different attestation structures (e.g., different record identifiers like a byte offset instead of a line number) are free to build their own seed and call
uuid5_from_seed()directly.- Parameters:
namespace (UUID) – The consumer’s attestation namespace UUID.
source_artifact_hash (str) – Stable hash of the source artifact (e.g., the file the record came from).
record_line (int) – The record’s identifier within the artifact (line number, row index, etc.).
parser_version (str) – Identifier of the parser version that produced the attested values.
values_hash (str) – Hash of the attested values for integrity.
- Returns:
A deterministic UUID5 combining the inputs.
- Raises:
ValueError – if any string input is empty, whitespace-only, or contains the RS delimiter (
\x1e).- Return type:
- siege_utilities.identifiers.derive_root(seed)[source]
Derive a root UUID5 namespace from a string seed.
Consumers pick a stable seed string representing their domain (e.g., “mydomain.example.com”). The derived UUID should be computed once, hardcoded in consumer code as a constant, and treated as immutable — changing it would invalidate every UUID derived under it.
Example:
>>> ROOT = derive_root("example.com") >>> ROOT # this value should be hardcoded in consumer code: UUID('a5cf6e8e-4cfa-5f31-a804-6de6d1245e26')
- Parameters:
seed (str) – A non-empty string. Convention: lowercase DNS-style name.
- Returns:
The UUID5 derived from
NAMESPACE_URLandseed.- Raises:
ValueError – if
seedis empty.- Return type:
- siege_utilities.identifiers.derive_sub_namespace(root, name)[source]
Derive a sub-namespace UUID5 from a root namespace + name.
Used for per-entity-type namespaces: a consumer with a root and a domain concept like “person” derives the PERSON_NS this way.
Example:
>>> ROOT = derive_root("example.com") >>> PERSON_NS = derive_sub_namespace(ROOT, "person") >>> # hardcode PERSON_NS in consumer code; assert-match in tests.
- Parameters:
- Returns:
The UUID5 derived from
rootandname.- Raises:
ValueError – if
nameis empty.- Return type:
- siege_utilities.identifiers.normalize_name_v1(name)[source]
Normalize a human-readable name to a stable seed string.
Rules (v1, frozen): - NFKD decomposition, drop Unicode combining marks (strips accents) - Casefold (lowercase; locale-independent) - Strip punctuation except word characters and whitespace - Collapse multi-whitespace to single space - Strip leading/trailing whitespace
Returns an empty string if input is
Noneor empty after normalization. Consumers that need a fallback seed ladder should detect empty output and fall through to a different identifier.This function MUST NOT be modified. New normalization behavior goes in a new version function.
- siege_utilities.identifiers.uuid5_from_seed(namespace, seed)[source]
Generate a deterministic UUID5 under a given namespace.
- Parameters:
- Returns:
The UUID5 derived from
namespaceandseed.- Raises:
ValueError – if
seedis empty or whitespace-only.- Return type:
Submodules
UUID5 namespace derivation helpers.
Consumers declare their own root seed and per-entity-type sub-namespaces using these factory functions, then hardcode the resulting UUIDs as immutable constants in their own code. The consumer’s tests should verify that the hardcoded values match the derivation (same pattern shown in the docstrings below).
- siege_utilities.identifiers.namespaces.derive_root(seed)[source]
Derive a root UUID5 namespace from a string seed.
Consumers pick a stable seed string representing their domain (e.g., “mydomain.example.com”). The derived UUID should be computed once, hardcoded in consumer code as a constant, and treated as immutable — changing it would invalidate every UUID derived under it.
Example:
>>> ROOT = derive_root("example.com") >>> ROOT # this value should be hardcoded in consumer code: UUID('a5cf6e8e-4cfa-5f31-a804-6de6d1245e26')
- Parameters:
seed (str) – A non-empty string. Convention: lowercase DNS-style name.
- Returns:
The UUID5 derived from
NAMESPACE_URLandseed.- Raises:
ValueError – if
seedis empty.- Return type:
- siege_utilities.identifiers.namespaces.derive_sub_namespace(root, name)[source]
Derive a sub-namespace UUID5 from a root namespace + name.
Used for per-entity-type namespaces: a consumer with a root and a domain concept like “person” derives the PERSON_NS this way.
Example:
>>> ROOT = derive_root("example.com") >>> PERSON_NS = derive_sub_namespace(ROOT, "person") >>> # hardcode PERSON_NS in consumer code; assert-match in tests.
- Parameters:
- Returns:
The UUID5 derived from
rootandname.- Raises:
ValueError – if
nameis empty.- Return type:
Name-normalization functions for use as stable seeds to UUID5 generators.
Each version is pinned forever — consumers record which version was used to seed a given entity, and any change that would alter output for any input is a correctness violation (would drift UUIDs).
New normalization behavior goes in a new version (normalize_name_v2…),
never as a modification to an existing version. The consumer’s
entity_identifiers table (or equivalent) should carry a
canonical_seed_version column so the pinning is explicit.
- siege_utilities.identifiers.normalize.normalize_name_v1(name)[source]
Normalize a human-readable name to a stable seed string.
Rules (v1, frozen): - NFKD decomposition, drop Unicode combining marks (strips accents) - Casefold (lowercase; locale-independent) - Strip punctuation except word characters and whitespace - Collapse multi-whitespace to single space - Strip leading/trailing whitespace
Returns an empty string if input is
Noneor empty after normalization. Consumers that need a fallback seed ladder should detect empty output and fall through to a different identifier.This function MUST NOT be modified. New normalization behavior goes in a new version function.
Generic UUID5 generators — domain-agnostic.
Callers supply the namespace (derived via namespaces.derive_root /
derive_sub_namespace and hardcoded in their own code) plus the seed.
This module knows nothing about Persons, Committees, Attestations as
domain concepts — it provides the deterministic generator only.
For entities that are resolved artifacts (not pre-existing-identity
entities with stable external IDs), callers should use uuid.uuid4()
directly, not these helpers.
- siege_utilities.identifiers.uuid_generation.attestation_uuid(*, namespace, source_artifact_hash, record_line, parser_version, values_hash)[source]
Generate a parser-version-aware attestation UUID.
Re-processing the same source line with the same parser version produces the same UUID (write-path idempotency). Bumping the parser version produces a new UUID, so the re-parse is correctly treated as a new attestation rather than silently clobbering the old one.
The seed is built as
RS-delimited components (source_artifact_hash RS record_line RS parser_version RS values_hash), whereRSis ASCII 0x1E (Record Separator). This delimiter cannot appear in hash strings, version tags, or file paths, so there are no component-boundary collisions regardless of component content.This helper is deliberately generic about what an “attestation” is — it just requires a namespace and four stable inputs. Consumers with different attestation structures (e.g., different record identifiers like a byte offset instead of a line number) are free to build their own seed and call
uuid5_from_seed()directly.- Parameters:
namespace (UUID) – The consumer’s attestation namespace UUID.
source_artifact_hash (str) – Stable hash of the source artifact (e.g., the file the record came from).
record_line (int) – The record’s identifier within the artifact (line number, row index, etc.).
parser_version (str) – Identifier of the parser version that produced the attested values.
values_hash (str) – Hash of the attested values for integrity.
- Returns:
A deterministic UUID5 combining the inputs.
- Raises:
ValueError – if any string input is empty, whitespace-only, or contains the RS delimiter (
\x1e).- Return type:
- siege_utilities.identifiers.uuid_generation.uuid5_from_seed(namespace, seed)[source]
Generate a deterministic UUID5 under a given namespace.
- Parameters:
- Returns:
The UUID5 derived from
namespaceandseed.- Raises:
ValueError – if
seedis empty or whitespace-only.- Return type: