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), where RS is 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:

UUID

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_URL and seed.

Raises:

ValueError – if seed is empty.

Return type:

UUID

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:
  • root (UUID) – The root namespace UUID (typically from derive_root).

  • name (str) – Non-empty string identifying the sub-namespace purpose. Convention: lowercase snake_case, stable for the lifetime of the consumer.

Returns:

The UUID5 derived from root and name.

Raises:

ValueError – if name is empty.

Return type:

UUID

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 None or 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.

Parameters:

name (str)

Return type:

str

siege_utilities.identifiers.uuid5_from_seed(namespace, seed)[source]

Generate a deterministic UUID5 under a given namespace.

Parameters:
  • namespace (UUID) – A namespace UUID (typically a per-entity-type sub-namespace).

  • seed (str) – Non-empty string — the canonical identifier input. Consumers are responsible for seed-ladder logic (picking the best available identifier at resolution time).

Returns:

The UUID5 derived from namespace and seed.

Raises:

ValueError – if seed is empty or whitespace-only.

Return type:

UUID

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_URL and seed.

Raises:

ValueError – if seed is empty.

Return type:

UUID

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:
  • root (UUID) – The root namespace UUID (typically from derive_root).

  • name (str) – Non-empty string identifying the sub-namespace purpose. Convention: lowercase snake_case, stable for the lifetime of the consumer.

Returns:

The UUID5 derived from root and name.

Raises:

ValueError – if name is empty.

Return type:

UUID

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 None or 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.

Parameters:

name (str)

Return type:

str

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), where RS is 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:

UUID

siege_utilities.identifiers.uuid_generation.uuid5_from_seed(namespace, seed)[source]

Generate a deterministic UUID5 under a given namespace.

Parameters:
  • namespace (UUID) – A namespace UUID (typically a per-entity-type sub-namespace).

  • seed (str) – Non-empty string — the canonical identifier input. Consumers are responsible for seed-ladder logic (picking the best available identifier at resolution time).

Returns:

The UUID5 derived from namespace and seed.

Raises:

ValueError – if seed is empty or whitespace-only.

Return type:

UUID