Skip to content

API Reference

Auto-generated code documentation.

civic_exchange_protocol

Civic Exchange Protocol (CEP) - Python Implementation.

A protocol for transparent, verifiable civic data exchange.

Submodules

core: Core types (timestamps, hashes, attestations) core_linker: SNFEI generation and entity normalization entity: Entity records and identifiers relationship: Relationship records exchange: Exchange records

api

CEP Entity Canonicalization Service.

This service generates the Canonical String and Entity Hash for a CEP Entity Record, enforcing strict field ordering and temporal rules.

Dependencies: fastapi, uvicorn, pydantic, hashlib, datetime, decimal To run: uvicorn cep_entity_service:app --reload

CanonicalResponse

Bases: BaseModel

Defines the structure of the API's successful output.

Source code in src/python/src/civic_exchange_protocol/api.py
186
187
188
189
190
191
192
class CanonicalResponse(BaseModel):
    """Defines the structure of the API's successful output."""

    c_string: str = Field(
        ..., description="The Canonical String (C-String) used as the hash input."
    )
    entity_hash: str = Field(..., description="The final 64-character SHA-256 Entity Hash.")

EntityPayload

Bases: BaseModel

Defines the structure for the CEP Entity Record.

Source code in src/python/src/civic_exchange_protocol/api.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class EntityPayload(BaseModel):
    """Defines the structure for the CEP Entity Record."""

    # Section 1: Identity and Attestation
    entity_uei: str = Field(..., pattern=r"^[A-Z0-9]{12}$", alias="entityUei")
    record_id: str = Field(..., max_length=64, alias="recordId")
    attesting_uei: str = Field(..., pattern=r"^[A-Z0-9]{12}$", alias="attestingUei")
    attestation_timestamp: datetime = Field(
        ..., description="ISO 8601 UTC with microsecond precision.", alias="attestationTimestamp"
    )

    # Section 2: Temporal Status and Governance (The critical fields)
    # Section 2: Temporal Status and Governance (The critical fields)
    status_effective_date: datetime = Field(
        ...,
        description="The 'As-of' date/time when this record became valid.",
        alias="statusEffectiveDate",
    )
    status_termination_date: datetime | None = Field(
        None,
        description="The date/time the record ceased to be valid (omitted if null).",
        alias="statusTerminationDate",
    )
    legal_status: str = Field(
        ..., description="e.g., ACTIVE, DISSOLVED, SUSPENDED.", alias="legalStatus"
    )
    status_suspension_date: datetime | None = Field(
        None,
        description="Date/time the entity was suspended (omitted if null).",
        alias="statusSuspensionDate",
    )
    # Section 3: Core Attributes
    legal_name: str = Field(..., max_length=256, alias="legalName")
    tax_id: str = Field(..., max_length=32, alias="taxId")
    physical_address_line1: str = Field(..., max_length=128, alias="physicalAddressLine1")
    physical_address_city: str = Field(..., max_length=64, alias="physicalAddressCity")
    physical_address_postal_code: str = Field(..., max_length=16, alias="physicalAddressPostalCode")
    is_government: bool = Field(
        ..., description="True if a recognized government body.", alias="isGovernment"
    )
    naics_code: str | None = Field(None, max_length=10, alias="naicsCode")

    class Config:
        """Pydantic model configuration for JSON serialization and schema examples."""

        json_encoders = {datetime: lambda v: v.isoformat().replace("+00:00", "Z")}
        populate_by_name = True
        # Example for API documentation
        schema_extra = {
            "example": {
                "entityUei": "1A2B3C4D5E6F",
                "recordId": "CEP-2025-001",
                "attestingUei": "GOV-0000000001",
                "attestationTimestamp": "2025-11-27T17:52:30.123456Z",
                "statusEffectiveDate": "2024-01-01T00:00:00Z",
                # statusTerminationDate is None/omitted, implying current validity
                "legalStatus": "ACTIVE",
                "statusSuspensionDate": None,  # Omitted
                "legalName": "Acme Data Solutions LLC",
                "taxId": "99-1234567",
                "physicalAddressLine1": "123 Main St.",
                "physicalAddressCity": "Springfield",
                "physicalAddressPostalCode": "62704",
                "isGovernment": False,
                "naicsCode": "541512",
            }
        }
Config

Pydantic model configuration for JSON serialization and schema examples.

Source code in src/python/src/civic_exchange_protocol/api.py
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class Config:
    """Pydantic model configuration for JSON serialization and schema examples."""

    json_encoders = {datetime: lambda v: v.isoformat().replace("+00:00", "Z")}
    populate_by_name = True
    # Example for API documentation
    schema_extra = {
        "example": {
            "entityUei": "1A2B3C4D5E6F",
            "recordId": "CEP-2025-001",
            "attestingUei": "GOV-0000000001",
            "attestationTimestamp": "2025-11-27T17:52:30.123456Z",
            "statusEffectiveDate": "2024-01-01T00:00:00Z",
            # statusTerminationDate is None/omitted, implying current validity
            "legalStatus": "ACTIVE",
            "statusSuspensionDate": None,  # Omitted
            "legalName": "Acme Data Solutions LLC",
            "taxId": "99-1234567",
            "physicalAddressLine1": "123 Main St.",
            "physicalAddressCity": "Springfield",
            "physicalAddressPostalCode": "62704",
            "isGovernment": False,
            "naicsCode": "541512",
        }
    }

canonicalize_entity async

canonicalize_entity(payload: EntityPayload)

Receives an Entity Record, performs canonical serialization (CAOS), and returns the cryptographic entity hash.

Performs canonical serialization (CAOS) on the entity record.

Source code in src/python/src/civic_exchange_protocol/api.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
@app.post("/api/v1/entity/canonicalize", response_model=CanonicalResponse, status_code=200)
async def canonicalize_entity(payload: EntityPayload):
    """Receives an Entity Record, performs canonical serialization (CAOS), and returns the cryptographic entity hash.

    Performs canonical serialization (CAOS) on the entity record.
    """
    try:
        # 1. Generate the Canonical String (C-String)
        c_string = generate_canonical_string(payload)

        # 2. Generate the Entity Hash
        entity_hash = generate_entity_hash(c_string)

        return CanonicalResponse(c_string=c_string, entity_hash=entity_hash)

    except Exception as e:
        print(f"Error during canonicalization: {e}")
        # In a production system, detailed logs would be captured here.
        raise HTTPException(status_code=500, detail="Internal canonicalization error.") from e

generate_canonical_string

generate_canonical_string(data: EntityPayload) -> str

Generate the pipe-delimited Canonical String (C-String) based on CAOS.

Source code in src/python/src/civic_exchange_protocol/api.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def generate_canonical_string(data: EntityPayload) -> str:
    """Generate the pipe-delimited Canonical String (C-String) based on CAOS."""
    parts = []

    # Convert Pydantic model to a dictionary. Use by_alias=False to use the Python field names.
    # Exclude None is handled explicitly in the loop for consistency with CEP Rule 1.2
    data_dict = data.dict(exclude_none=False)

    for field_name in CANONICAL_ATTRIBUTE_ORDER:
        value = data_dict.get(field_name)

        # Rule 1.2: Field Omission (Null/Empty Exclusion)
        if value is None or (isinstance(value, str) and value == ""):
            continue

        formatted_value = ""

        # Specific rule for microsecond precision on attestationTimestamp
        if field_name == "attestationTimestamp":
            if not isinstance(value, datetime):
                raise TypeError(f"attestationTimestamp must be datetime, got {type(value)!r}")
            formatted_value = _format_datetime(value)

        # Standard ISO 8601 for other temporal fields
        elif field_name in ["statusEffectiveDate", "statusTerminationDate", "statusSuspensionDate"]:
            # These are date-like; accept both date and datetime
            if not isinstance(value, datetime | date):
                raise TypeError(f"{field_name} must be date/datetime, got {type(value)!r}")
            formatted_value = value.isoformat().replace("+00:00", "Z")

        # Standard Boolean Formatting
        elif field_name == "isGovernment":
            formatted_value = "true" if value else "false"

        # Standard String/Integer Fields
        else:
            formatted_value = str(value)

        parts.append(formatted_value)

    # Rule 1.3: Join all parts with the pipe delimiter
    return "|".join(parts)

generate_entity_hash

generate_entity_hash(c_string: str) -> str

Generate the final SHA-256 Entity Hash.

Source code in src/python/src/civic_exchange_protocol/api.py
172
173
174
def generate_entity_hash(c_string: str) -> str:
    """Generate the final SHA-256 Entity Hash."""
    return hashlib.sha256(c_string.encode("utf-8")).hexdigest()

cli

cli

Command-line interface for the Civic Exchange Protocol.

This module provides CLI commands for: - version: Display the package version - validate: Validate exchange protocol data

validate
validate()

Validate exchange protocol data.

Source code in src/python/src/civic_exchange_protocol/cli/cli.py
21
22
23
24
@app.command()
def validate():
    """Validate exchange protocol data."""
    print("Validator coming soon.")
version
version()

Show package version.

Source code in src/python/src/civic_exchange_protocol/cli/cli.py
13
14
15
16
17
18
@app.command()
def version():
    """Show package version."""
    from civic_exchange_protocol import __version__

    print(__version__)

core

CEP Core - Core primitives for the Civic Exchange Protocol.

This package provides the foundational types used by all CEP record types:

  • CanonicalTimestamp: Microsecond-precision UTC timestamps
  • CanonicalHash: SHA-256 hash values
  • Canonicalize: Base class for deterministic serialization
  • Attestation: Cryptographic proof of record integrity

Attestation dataclass

Bases: Canonicalize

Cryptographic attestation proving record authenticity and integrity.

This structure aligns with W3C Verifiable Credentials Data Integrity.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@dataclass
class Attestation(Canonicalize):
    """Cryptographic attestation proving record authenticity and integrity.

    This structure aligns with W3C Verifiable Credentials Data Integrity.
    """

    # Verifiable ID of the entity or node attesting to this record
    attestor_id: str

    # When the attestation was created
    attestation_timestamp: CanonicalTimestamp

    # The proof algorithm identifier
    # Examples: "Ed25519Signature2020", "EcdsaSecp256k1Signature2019", "DataIntegrityProof"
    proof_type: str

    # The cryptographic signature or proof value
    proof_value: str

    # URI resolving to the public key or DID document for verification
    verification_method_uri: str

    # The purpose of the proof
    proof_purpose: ProofPurpose = field(default=ProofPurpose.ASSERTION_METHOD)

    # Optional URI to a timestamping authority or DLT anchor
    anchor_uri: str | None = None

    @classmethod
    def new(
        cls,
        attestor_id: str,
        attestation_timestamp: CanonicalTimestamp,
        proof_type: str,
        proof_value: str,
        verification_method_uri: str,
    ) -> "Attestation":
        """Create a new Attestation with required fields."""
        return cls(
            attestor_id=attestor_id,
            attestation_timestamp=attestation_timestamp,
            proof_type=proof_type,
            proof_value=proof_value,
            verification_method_uri=verification_method_uri,
        )

    def with_purpose(self, purpose: ProofPurpose) -> "Attestation":
        """Return a new Attestation with the specified proof purpose."""
        return Attestation(
            attestor_id=self.attestor_id,
            attestation_timestamp=self.attestation_timestamp,
            proof_type=self.proof_type,
            proof_value=self.proof_value,
            verification_method_uri=self.verification_method_uri,
            proof_purpose=purpose,
            anchor_uri=self.anchor_uri,
        )

    def with_anchor(self, uri: str) -> "Attestation":
        """Return a new Attestation with the specified anchor URI."""
        return Attestation(
            attestor_id=self.attestor_id,
            attestation_timestamp=self.attestation_timestamp,
            proof_type=self.proof_type,
            proof_value=self.proof_value,
            verification_method_uri=self.verification_method_uri,
            proof_purpose=self.proof_purpose,
            anchor_uri=uri,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # Fields in alphabetical order
        insert_if_present(fields, "anchorUri", self.anchor_uri)
        insert_required(
            fields,
            "attestationTimestamp",
            self.attestation_timestamp.to_canonical_string(),
        )
        insert_required(fields, "attestorId", self.attestor_id)
        insert_required(fields, "proofPurpose", self.proof_purpose.as_str())
        insert_required(fields, "proofType", self.proof_type)
        insert_required(fields, "proofValue", self.proof_value)
        insert_required(fields, "verificationMethodUri", self.verification_method_uri)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # Fields in alphabetical order
    insert_if_present(fields, "anchorUri", self.anchor_uri)
    insert_required(
        fields,
        "attestationTimestamp",
        self.attestation_timestamp.to_canonical_string(),
    )
    insert_required(fields, "attestorId", self.attestor_id)
    insert_required(fields, "proofPurpose", self.proof_purpose.as_str())
    insert_required(fields, "proofType", self.proof_type)
    insert_required(fields, "proofValue", self.proof_value)
    insert_required(fields, "verificationMethodUri", self.verification_method_uri)

    return fields
new classmethod
new(
    attestor_id: str,
    attestation_timestamp: CanonicalTimestamp,
    proof_type: str,
    proof_value: str,
    verification_method_uri: str,
) -> Attestation

Create a new Attestation with required fields.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@classmethod
def new(
    cls,
    attestor_id: str,
    attestation_timestamp: CanonicalTimestamp,
    proof_type: str,
    proof_value: str,
    verification_method_uri: str,
) -> "Attestation":
    """Create a new Attestation with required fields."""
    return cls(
        attestor_id=attestor_id,
        attestation_timestamp=attestation_timestamp,
        proof_type=proof_type,
        proof_value=proof_value,
        verification_method_uri=verification_method_uri,
    )
with_anchor
with_anchor(uri: str) -> Attestation

Return a new Attestation with the specified anchor URI.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
87
88
89
90
91
92
93
94
95
96
97
def with_anchor(self, uri: str) -> "Attestation":
    """Return a new Attestation with the specified anchor URI."""
    return Attestation(
        attestor_id=self.attestor_id,
        attestation_timestamp=self.attestation_timestamp,
        proof_type=self.proof_type,
        proof_value=self.proof_value,
        verification_method_uri=self.verification_method_uri,
        proof_purpose=self.proof_purpose,
        anchor_uri=uri,
    )
with_purpose
with_purpose(purpose: ProofPurpose) -> Attestation

Return a new Attestation with the specified proof purpose.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
75
76
77
78
79
80
81
82
83
84
85
def with_purpose(self, purpose: ProofPurpose) -> "Attestation":
    """Return a new Attestation with the specified proof purpose."""
    return Attestation(
        attestor_id=self.attestor_id,
        attestation_timestamp=self.attestation_timestamp,
        proof_type=self.proof_type,
        proof_value=self.proof_value,
        verification_method_uri=self.verification_method_uri,
        proof_purpose=purpose,
        anchor_uri=self.anchor_uri,
    )

CanonicalHash

A SHA-256 hash value represented as a 64-character lowercase hex string.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class CanonicalHash:
    """A SHA-256 hash value represented as a 64-character lowercase hex string."""

    __slots__ = ("_hex",)

    def __init__(self, hex_value: str) -> None:
        """Create a CanonicalHash from a hex string.

        Args:
            hex_value: A 64-character hexadecimal string.

        Raises:
            ValueError: If the string is not valid.
        """
        if len(hex_value) != 64:
            raise ValueError(f"Hash must be 64 hex characters, got {len(hex_value)}")
        if not all(c in "0123456789abcdefABCDEF" for c in hex_value):
            raise ValueError("Hash must contain only hexadecimal characters")
        self._hex = hex_value.lower()

    @classmethod
    def from_canonical_string(cls, canonical: str) -> "CanonicalHash":
        """Compute the SHA-256 hash of the given canonical string.

        Args:
            canonical: The canonical string to hash.

        Returns:
            A CanonicalHash instance.
        """
        hasher = hashlib.sha256()
        hasher.update(canonical.encode("utf-8"))
        return cls(hasher.hexdigest())

    @classmethod
    def from_hex(cls, hex_value: str) -> Optional["CanonicalHash"]:
        """Create a CanonicalHash from a pre-computed hex string.

        Args:
            hex_value: A hexadecimal string.

        Returns:
            A CanonicalHash instance, or None if invalid.
        """
        try:
            return cls(hex_value)
        except ValueError:
            return None

    def as_hex(self) -> str:
        """Return the hash as a lowercase hex string."""
        return self._hex

    def as_bytes(self) -> bytes:
        """Return the hash as bytes (32 bytes)."""
        return bytes.fromhex(self._hex)

    def __str__(self) -> str:
        """Return a string representation of the hash."""
        return self._hex

    def __repr__(self) -> str:
        """Return a detailed string representation of the hash."""
        return f"CanonicalHash({self._hex!r})"

    def __eq__(self, other: object) -> bool:
        """Check equality with another CanonicalHash instance."""
        if isinstance(other, CanonicalHash):
            return self._hex == other._hex
        return NotImplemented

    def __hash__(self) -> int:
        """Return a hash value for the CanonicalHash instance."""
        return hash(self._hex)
__eq__
__eq__(other: object) -> bool

Check equality with another CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
75
76
77
78
79
def __eq__(self, other: object) -> bool:
    """Check equality with another CanonicalHash instance."""
    if isinstance(other, CanonicalHash):
        return self._hex == other._hex
    return NotImplemented
__hash__
__hash__() -> int

Return a hash value for the CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
81
82
83
def __hash__(self) -> int:
    """Return a hash value for the CanonicalHash instance."""
    return hash(self._hex)
__init__
__init__(hex_value: str) -> None

Create a CanonicalHash from a hex string.

Parameters:

Name Type Description Default
hex_value str

A 64-character hexadecimal string.

required

Raises:

Type Description
ValueError

If the string is not valid.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def __init__(self, hex_value: str) -> None:
    """Create a CanonicalHash from a hex string.

    Args:
        hex_value: A 64-character hexadecimal string.

    Raises:
        ValueError: If the string is not valid.
    """
    if len(hex_value) != 64:
        raise ValueError(f"Hash must be 64 hex characters, got {len(hex_value)}")
    if not all(c in "0123456789abcdefABCDEF" for c in hex_value):
        raise ValueError("Hash must contain only hexadecimal characters")
    self._hex = hex_value.lower()
__repr__
__repr__() -> str

Return a detailed string representation of the hash.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
71
72
73
def __repr__(self) -> str:
    """Return a detailed string representation of the hash."""
    return f"CanonicalHash({self._hex!r})"
__str__
__str__() -> str

Return a string representation of the hash.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
67
68
69
def __str__(self) -> str:
    """Return a string representation of the hash."""
    return self._hex
as_bytes
as_bytes() -> bytes

Return the hash as bytes (32 bytes).

Source code in src/python/src/civic_exchange_protocol/core/hash.py
63
64
65
def as_bytes(self) -> bytes:
    """Return the hash as bytes (32 bytes)."""
    return bytes.fromhex(self._hex)
as_hex
as_hex() -> str

Return the hash as a lowercase hex string.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
59
60
61
def as_hex(self) -> str:
    """Return the hash as a lowercase hex string."""
    return self._hex
from_canonical_string classmethod
from_canonical_string(canonical: str) -> CanonicalHash

Compute the SHA-256 hash of the given canonical string.

Parameters:

Name Type Description Default
canonical str

The canonical string to hash.

required

Returns:

Type Description
CanonicalHash

A CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
30
31
32
33
34
35
36
37
38
39
40
41
42
@classmethod
def from_canonical_string(cls, canonical: str) -> "CanonicalHash":
    """Compute the SHA-256 hash of the given canonical string.

    Args:
        canonical: The canonical string to hash.

    Returns:
        A CanonicalHash instance.
    """
    hasher = hashlib.sha256()
    hasher.update(canonical.encode("utf-8"))
    return cls(hasher.hexdigest())
from_hex classmethod
from_hex(hex_value: str) -> Optional[CanonicalHash]

Create a CanonicalHash from a pre-computed hex string.

Parameters:

Name Type Description Default
hex_value str

A hexadecimal string.

required

Returns:

Type Description
Optional[CanonicalHash]

A CanonicalHash instance, or None if invalid.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@classmethod
def from_hex(cls, hex_value: str) -> Optional["CanonicalHash"]:
    """Create a CanonicalHash from a pre-computed hex string.

    Args:
        hex_value: A hexadecimal string.

    Returns:
        A CanonicalHash instance, or None if invalid.
    """
    try:
        return cls(hex_value)
    except ValueError:
        return None

CanonicalTimestamp

A canonical CEP timestamp with mandatory microsecond precision.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class CanonicalTimestamp:
    """A canonical CEP timestamp with mandatory microsecond precision."""

    __slots__ = ("_dt",)

    def __init__(self, dt: datetime) -> None:
        """Create a new CanonicalTimestamp from a datetime.

        Args:
            dt: A datetime object. If naive, assumed to be UTC.
                If aware, will be converted to UTC.
        """
        if dt.tzinfo is None:
            # Naive datetime - assume UTC
            self._dt = dt.replace(tzinfo=UTC)
        else:
            # Convert to UTC
            self._dt = dt.astimezone(UTC)

    @classmethod
    def now(cls) -> "CanonicalTimestamp":
        """Return the current UTC time as a CanonicalTimestamp."""
        return cls(datetime.now(UTC))

    @classmethod
    def parse(cls, s: str) -> "CanonicalTimestamp":
        """Parse an ISO 8601 timestamp string.

        Accepts formats:
        - 2025-11-28T14:30:00.123456Z
        - 2025-11-28T14:30:00.123456+00:00
        - 2025-11-28T14:30:00Z (will add .000000)

        Args:
            s: The timestamp string to parse.

        Returns:
            A CanonicalTimestamp instance.

        Raises:
            ValueError: If the string cannot be parsed.
        """
        # Handle Z suffix
        if s.endswith("Z"):
            s = s[:-1] + "+00:00"

        # Try parsing with microseconds
        try:
            dt = datetime.fromisoformat(s)
            return cls(dt)
        except ValueError:
            pass

        # Try without microseconds and add them
        try:
            # Remove timezone for parsing, then add back
            if "+" in s:
                base, tz = s.rsplit("+", 1)
                dt = datetime.fromisoformat(base)
                dt = dt.replace(tzinfo=UTC)
                return cls(dt)
            if s.count("-") > 2:  # Has negative offset
                base, tz = s.rsplit("-", 1)
                dt = datetime.fromisoformat(base)
                dt = dt.replace(tzinfo=UTC)
                return cls(dt)
        except ValueError:
            pass

        raise ValueError(f"Cannot parse timestamp: {s}")

    def as_datetime(self) -> datetime:
        """Return the underlying datetime object (UTC)."""
        return self._dt

    def to_canonical_string(self) -> str:
        """Return the canonical string representation.

        Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

        This format is REQUIRED for hash stability across all CEP implementations.
        """
        return self._dt.strftime(CANONICAL_FORMAT)

    def __str__(self) -> str:
        """Return the canonical string representation of the timestamp."""
        return self.to_canonical_string()

    def __repr__(self) -> str:
        """Return the developer-friendly representation of the timestamp."""
        return f"CanonicalTimestamp({self.to_canonical_string()!r})"

    def __eq__(self, other: object) -> bool:
        """Check equality with another CanonicalTimestamp."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt == other._dt
        return NotImplemented

    def __lt__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is less than another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt < other._dt
        return NotImplemented

    def __le__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is less than or equal to another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt <= other._dt
        return NotImplemented

    def __gt__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is greater than another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt > other._dt
        return NotImplemented

    def __ge__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is greater than or equal to another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt >= other._dt
        return NotImplemented

    def __hash__(self) -> int:
        """Return the hash of the timestamp."""
        return hash(self._dt)
__eq__
__eq__(other: object) -> bool

Check equality with another CanonicalTimestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
109
110
111
112
113
def __eq__(self, other: object) -> bool:
    """Check equality with another CanonicalTimestamp."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt == other._dt
    return NotImplemented
__ge__
__ge__(other: CanonicalTimestamp) -> bool

Check if this timestamp is greater than or equal to another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
133
134
135
136
137
def __ge__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is greater than or equal to another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt >= other._dt
    return NotImplemented
__gt__
__gt__(other: CanonicalTimestamp) -> bool

Check if this timestamp is greater than another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
127
128
129
130
131
def __gt__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is greater than another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt > other._dt
    return NotImplemented
__hash__
__hash__() -> int

Return the hash of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
139
140
141
def __hash__(self) -> int:
    """Return the hash of the timestamp."""
    return hash(self._dt)
__init__
__init__(dt: datetime) -> None

Create a new CanonicalTimestamp from a datetime.

Parameters:

Name Type Description Default
dt datetime

A datetime object. If naive, assumed to be UTC. If aware, will be converted to UTC.

required
Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, dt: datetime) -> None:
    """Create a new CanonicalTimestamp from a datetime.

    Args:
        dt: A datetime object. If naive, assumed to be UTC.
            If aware, will be converted to UTC.
    """
    if dt.tzinfo is None:
        # Naive datetime - assume UTC
        self._dt = dt.replace(tzinfo=UTC)
    else:
        # Convert to UTC
        self._dt = dt.astimezone(UTC)
__le__
__le__(other: CanonicalTimestamp) -> bool

Check if this timestamp is less than or equal to another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
121
122
123
124
125
def __le__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is less than or equal to another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt <= other._dt
    return NotImplemented
__lt__
__lt__(other: CanonicalTimestamp) -> bool

Check if this timestamp is less than another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
115
116
117
118
119
def __lt__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is less than another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt < other._dt
    return NotImplemented
__repr__
__repr__() -> str

Return the developer-friendly representation of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
105
106
107
def __repr__(self) -> str:
    """Return the developer-friendly representation of the timestamp."""
    return f"CanonicalTimestamp({self.to_canonical_string()!r})"
__str__
__str__() -> str

Return the canonical string representation of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
101
102
103
def __str__(self) -> str:
    """Return the canonical string representation of the timestamp."""
    return self.to_canonical_string()
as_datetime
as_datetime() -> datetime

Return the underlying datetime object (UTC).

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
88
89
90
def as_datetime(self) -> datetime:
    """Return the underlying datetime object (UTC)."""
    return self._dt
now classmethod
now() -> CanonicalTimestamp

Return the current UTC time as a CanonicalTimestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
36
37
38
39
@classmethod
def now(cls) -> "CanonicalTimestamp":
    """Return the current UTC time as a CanonicalTimestamp."""
    return cls(datetime.now(UTC))
parse classmethod
parse(s: str) -> CanonicalTimestamp

Parse an ISO 8601 timestamp string.

Accepts formats: - 2025-11-28T14:30:00.123456Z - 2025-11-28T14:30:00.123456+00:00 - 2025-11-28T14:30:00Z (will add .000000)

Parameters:

Name Type Description Default
s str

The timestamp string to parse.

required

Returns:

Type Description
CanonicalTimestamp

A CanonicalTimestamp instance.

Raises:

Type Description
ValueError

If the string cannot be parsed.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@classmethod
def parse(cls, s: str) -> "CanonicalTimestamp":
    """Parse an ISO 8601 timestamp string.

    Accepts formats:
    - 2025-11-28T14:30:00.123456Z
    - 2025-11-28T14:30:00.123456+00:00
    - 2025-11-28T14:30:00Z (will add .000000)

    Args:
        s: The timestamp string to parse.

    Returns:
        A CanonicalTimestamp instance.

    Raises:
        ValueError: If the string cannot be parsed.
    """
    # Handle Z suffix
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"

    # Try parsing with microseconds
    try:
        dt = datetime.fromisoformat(s)
        return cls(dt)
    except ValueError:
        pass

    # Try without microseconds and add them
    try:
        # Remove timezone for parsing, then add back
        if "+" in s:
            base, tz = s.rsplit("+", 1)
            dt = datetime.fromisoformat(base)
            dt = dt.replace(tzinfo=UTC)
            return cls(dt)
        if s.count("-") > 2:  # Has negative offset
            base, tz = s.rsplit("-", 1)
            dt = datetime.fromisoformat(base)
            dt = dt.replace(tzinfo=UTC)
            return cls(dt)
    except ValueError:
        pass

    raise ValueError(f"Cannot parse timestamp: {s}")
to_canonical_string
to_canonical_string() -> str

Return the canonical string representation.

Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

This format is REQUIRED for hash stability across all CEP implementations.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
92
93
94
95
96
97
98
99
def to_canonical_string(self) -> str:
    """Return the canonical string representation.

    Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

    This format is REQUIRED for hash stability across all CEP implementations.
    """
    return self._dt.strftime(CANONICAL_FORMAT)

Canonicalize

Bases: ABC

Base class for types that can be serialized to a canonical string for hashing.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Canonicalize(ABC):
    """Base class for types that can be serialized to a canonical string for hashing."""

    @abstractmethod
    def canonical_fields(self) -> dict[str, str]:
        """Return the ordered map of field names to their canonical string values.

        Fields with None/null/empty values should NOT be included in the dict.
        The dict will be sorted alphabetically by key.

        Returns:
            A dictionary of field names to string values.
        """
        pass

    def to_canonical_string(self) -> str:
        """Generate the canonical string representation for hashing.

        Format: "field1":"value1","field2":"value2",...

        Fields are ordered alphabetically by key.

        Returns:
            The canonical string.
        """
        fields = self.canonical_fields()
        # Sort by key alphabetically
        sorted_items = sorted(fields.items(), key=lambda x: x[0])
        parts = [f'"{k}":"{v}"' for k, v in sorted_items]
        return ",".join(parts)

    def calculate_hash(self) -> CanonicalHash:
        """Compute the SHA-256 hash of the canonical string.

        Returns:
            A CanonicalHash instance.
        """
        return CanonicalHash.from_canonical_string(self.to_canonical_string())
calculate_hash
calculate_hash() -> CanonicalHash

Compute the SHA-256 hash of the canonical string.

Returns:

Type Description
CanonicalHash

A CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
57
58
59
60
61
62
63
def calculate_hash(self) -> CanonicalHash:
    """Compute the SHA-256 hash of the canonical string.

    Returns:
        A CanonicalHash instance.
    """
    return CanonicalHash.from_canonical_string(self.to_canonical_string())
canonical_fields abstractmethod
canonical_fields() -> dict[str, str]

Return the ordered map of field names to their canonical string values.

Fields with None/null/empty values should NOT be included in the dict. The dict will be sorted alphabetically by key.

Returns:

Type Description
dict[str, str]

A dictionary of field names to string values.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def canonical_fields(self) -> dict[str, str]:
    """Return the ordered map of field names to their canonical string values.

    Fields with None/null/empty values should NOT be included in the dict.
    The dict will be sorted alphabetically by key.

    Returns:
        A dictionary of field names to string values.
    """
    pass
to_canonical_string
to_canonical_string() -> str

Generate the canonical string representation for hashing.

Format: "field1":"value1","field2":"value2",...

Fields are ordered alphabetically by key.

Returns:

Type Description
str

The canonical string.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def to_canonical_string(self) -> str:
    """Generate the canonical string representation for hashing.

    Format: "field1":"value1","field2":"value2",...

    Fields are ordered alphabetically by key.

    Returns:
        The canonical string.
    """
    fields = self.canonical_fields()
    # Sort by key alphabetically
    sorted_items = sorted(fields.items(), key=lambda x: x[0])
    parts = [f'"{k}":"{v}"' for k, v in sorted_items]
    return ",".join(parts)

CepError

Bases: Exception

Base exception for CEP operations.

Source code in src/python/src/civic_exchange_protocol/core/error.py
4
5
6
7
class CepError(Exception):
    """Base exception for CEP operations."""

    pass

HashMismatchError

Bases: CepError

Hash verification failed.

Source code in src/python/src/civic_exchange_protocol/core/error.py
50
51
52
53
54
55
56
57
class HashMismatchError(CepError):
    """Hash verification failed."""

    def __init__(self, expected: str, actual: str) -> None:
        """Initialize with expected and actual hash values."""
        super().__init__(f"hash verification failed: expected {expected}, got {actual}")
        self.expected = expected
        self.actual = actual
__init__
__init__(expected: str, actual: str) -> None

Initialize with expected and actual hash values.

Source code in src/python/src/civic_exchange_protocol/core/error.py
53
54
55
56
57
def __init__(self, expected: str, actual: str) -> None:
    """Initialize with expected and actual hash values."""
    super().__init__(f"hash verification failed: expected {expected}, got {actual}")
    self.expected = expected
    self.actual = actual

InvalidHashError

Bases: CepError

Invalid hash format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
18
19
20
21
22
23
class InvalidHashError(CepError):
    """Invalid hash format."""

    def __init__(self, value: str) -> None:
        """Initialize with the invalid hash value."""
        super().__init__(f"invalid hash: expected 64 hex characters, got {value}")
__init__
__init__(value: str) -> None

Initialize with the invalid hash value.

Source code in src/python/src/civic_exchange_protocol/core/error.py
21
22
23
def __init__(self, value: str) -> None:
    """Initialize with the invalid hash value."""
    super().__init__(f"invalid hash: expected 64 hex characters, got {value}")

InvalidIdentifierError

Bases: CepError

Invalid identifier format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
26
27
28
29
30
31
class InvalidIdentifierError(CepError):
    """Invalid identifier format."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about invalid identifier."""
        super().__init__(f"invalid identifier: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about invalid identifier.

Source code in src/python/src/civic_exchange_protocol/core/error.py
29
30
31
def __init__(self, message: str) -> None:
    """Initialize with error message about invalid identifier."""
    super().__init__(f"invalid identifier: {message}")

InvalidTimestampError

Bases: CepError

Invalid timestamp format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
10
11
12
13
14
15
class InvalidTimestampError(CepError):
    """Invalid timestamp format."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about invalid timestamp."""
        super().__init__(f"invalid timestamp: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about invalid timestamp.

Source code in src/python/src/civic_exchange_protocol/core/error.py
13
14
15
def __init__(self, message: str) -> None:
    """Initialize with error message about invalid timestamp."""
    super().__init__(f"invalid timestamp: {message}")

MissingFieldError

Bases: CepError

Missing required field.

Source code in src/python/src/civic_exchange_protocol/core/error.py
34
35
36
37
38
39
class MissingFieldError(CepError):
    """Missing required field."""

    def __init__(self, field: str) -> None:
        """Initialize with the name of the missing field."""
        super().__init__(f"missing required field: {field}")
__init__
__init__(field: str) -> None

Initialize with the name of the missing field.

Source code in src/python/src/civic_exchange_protocol/core/error.py
37
38
39
def __init__(self, field: str) -> None:
    """Initialize with the name of the missing field."""
    super().__init__(f"missing required field: {field}")

ProofPurpose

Bases: Enum

The purpose of a cryptographic proof.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
16
17
18
19
20
21
22
23
24
25
class ProofPurpose(Enum):
    """The purpose of a cryptographic proof."""

    ASSERTION_METHOD = "assertionMethod"
    AUTHENTICATION = "authentication"
    CAPABILITY_DELEGATION = "capabilityDelegation"

    def as_str(self) -> str:
        """Return the canonical string representation."""
        return self.value
as_str
as_str() -> str

Return the canonical string representation.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
23
24
25
def as_str(self) -> str:
    """Return the canonical string representation."""
    return self.value

RevisionChainError

Bases: CepError

Revision chain error.

Source code in src/python/src/civic_exchange_protocol/core/error.py
60
61
62
63
64
65
class RevisionChainError(CepError):
    """Revision chain error."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about revision chain."""
        super().__init__(f"revision chain error: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about revision chain.

Source code in src/python/src/civic_exchange_protocol/core/error.py
63
64
65
def __init__(self, message: str) -> None:
    """Initialize with error message about revision chain."""
    super().__init__(f"revision chain error: {message}")

UnsupportedVersionError

Bases: CepError

Schema version mismatch.

Source code in src/python/src/civic_exchange_protocol/core/error.py
42
43
44
45
46
47
class UnsupportedVersionError(CepError):
    """Schema version mismatch."""

    def __init__(self, version: str) -> None:
        """Initialize with the unsupported version string."""
        super().__init__(f"unsupported schema version: {version}")
__init__
__init__(version: str) -> None

Initialize with the unsupported version string.

Source code in src/python/src/civic_exchange_protocol/core/error.py
45
46
47
def __init__(self, version: str) -> None:
    """Initialize with the unsupported version string."""
    super().__init__(f"unsupported schema version: {version}")

format_amount

format_amount(amount: float) -> str

Format a monetary amount with exactly 2 decimal places.

This ensures consistent formatting across all implementations: - 100 becomes "100.00" - 100.5 becomes "100.50" - 100.756 becomes "100.76" (rounded)

Parameters:

Name Type Description Default
amount float

The monetary amount.

required

Returns:

Type Description
str

The formatted string with exactly 2 decimal places.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def format_amount(amount: float) -> str:
    """Format a monetary amount with exactly 2 decimal places.

    This ensures consistent formatting across all implementations:
    - 100 becomes "100.00"
    - 100.5 becomes "100.50"
    - 100.756 becomes "100.76" (rounded)

    Args:
        amount: The monetary amount.

    Returns:
        The formatted string with exactly 2 decimal places.
    """
    # Use Decimal for precise rounding
    d = Decimal(str(amount))
    rounded = d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return f"{rounded:.2f}"

insert_if_present

insert_if_present(
    fields: dict[str, str], key: str, value: str | None
) -> None

Add a field to the dict only if the value is not None and not empty.

Parameters:

Name Type Description Default
fields dict[str, str]

The dictionary to add to.

required
key str

The field name.

required
value str | None

The field value (may be None or empty).

required
Source code in src/python/src/civic_exchange_protocol/core/canonical.py
86
87
88
89
90
91
92
93
94
95
def insert_if_present(fields: dict[str, str], key: str, value: str | None) -> None:
    """Add a field to the dict only if the value is not None and not empty.

    Args:
        fields: The dictionary to add to.
        key: The field name.
        value: The field value (may be None or empty).
    """
    if value is not None and value != "":
        fields[key] = value

insert_required

insert_required(
    fields: dict[str, str], key: str, value: str
) -> None

Add a required field to the dict.

Parameters:

Name Type Description Default
fields dict[str, str]

The dictionary to add to.

required
key str

The field name.

required
value str

The field value.

required
Source code in src/python/src/civic_exchange_protocol/core/canonical.py
 98
 99
100
101
102
103
104
105
106
def insert_required(fields: dict[str, str], key: str, value: str) -> None:
    """Add a required field to the dict.

    Args:
        fields: The dictionary to add to.
        key: The field name.
        value: The field value.
    """
    fields[key] = value

attestation

Attestation and cryptographic proof types for CEP records.

Every CEP record includes an attestation block that proves: - Who attested to the record (attestor_id) - When it was attested (attestation_timestamp) - Cryptographic proof of integrity (proof_type, proof_value, verification_method_uri)

Attestation dataclass

Bases: Canonicalize

Cryptographic attestation proving record authenticity and integrity.

This structure aligns with W3C Verifiable Credentials Data Integrity.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
@dataclass
class Attestation(Canonicalize):
    """Cryptographic attestation proving record authenticity and integrity.

    This structure aligns with W3C Verifiable Credentials Data Integrity.
    """

    # Verifiable ID of the entity or node attesting to this record
    attestor_id: str

    # When the attestation was created
    attestation_timestamp: CanonicalTimestamp

    # The proof algorithm identifier
    # Examples: "Ed25519Signature2020", "EcdsaSecp256k1Signature2019", "DataIntegrityProof"
    proof_type: str

    # The cryptographic signature or proof value
    proof_value: str

    # URI resolving to the public key or DID document for verification
    verification_method_uri: str

    # The purpose of the proof
    proof_purpose: ProofPurpose = field(default=ProofPurpose.ASSERTION_METHOD)

    # Optional URI to a timestamping authority or DLT anchor
    anchor_uri: str | None = None

    @classmethod
    def new(
        cls,
        attestor_id: str,
        attestation_timestamp: CanonicalTimestamp,
        proof_type: str,
        proof_value: str,
        verification_method_uri: str,
    ) -> "Attestation":
        """Create a new Attestation with required fields."""
        return cls(
            attestor_id=attestor_id,
            attestation_timestamp=attestation_timestamp,
            proof_type=proof_type,
            proof_value=proof_value,
            verification_method_uri=verification_method_uri,
        )

    def with_purpose(self, purpose: ProofPurpose) -> "Attestation":
        """Return a new Attestation with the specified proof purpose."""
        return Attestation(
            attestor_id=self.attestor_id,
            attestation_timestamp=self.attestation_timestamp,
            proof_type=self.proof_type,
            proof_value=self.proof_value,
            verification_method_uri=self.verification_method_uri,
            proof_purpose=purpose,
            anchor_uri=self.anchor_uri,
        )

    def with_anchor(self, uri: str) -> "Attestation":
        """Return a new Attestation with the specified anchor URI."""
        return Attestation(
            attestor_id=self.attestor_id,
            attestation_timestamp=self.attestation_timestamp,
            proof_type=self.proof_type,
            proof_value=self.proof_value,
            verification_method_uri=self.verification_method_uri,
            proof_purpose=self.proof_purpose,
            anchor_uri=uri,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # Fields in alphabetical order
        insert_if_present(fields, "anchorUri", self.anchor_uri)
        insert_required(
            fields,
            "attestationTimestamp",
            self.attestation_timestamp.to_canonical_string(),
        )
        insert_required(fields, "attestorId", self.attestor_id)
        insert_required(fields, "proofPurpose", self.proof_purpose.as_str())
        insert_required(fields, "proofType", self.proof_type)
        insert_required(fields, "proofValue", self.proof_value)
        insert_required(fields, "verificationMethodUri", self.verification_method_uri)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # Fields in alphabetical order
    insert_if_present(fields, "anchorUri", self.anchor_uri)
    insert_required(
        fields,
        "attestationTimestamp",
        self.attestation_timestamp.to_canonical_string(),
    )
    insert_required(fields, "attestorId", self.attestor_id)
    insert_required(fields, "proofPurpose", self.proof_purpose.as_str())
    insert_required(fields, "proofType", self.proof_type)
    insert_required(fields, "proofValue", self.proof_value)
    insert_required(fields, "verificationMethodUri", self.verification_method_uri)

    return fields
new classmethod
new(
    attestor_id: str,
    attestation_timestamp: CanonicalTimestamp,
    proof_type: str,
    proof_value: str,
    verification_method_uri: str,
) -> Attestation

Create a new Attestation with required fields.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@classmethod
def new(
    cls,
    attestor_id: str,
    attestation_timestamp: CanonicalTimestamp,
    proof_type: str,
    proof_value: str,
    verification_method_uri: str,
) -> "Attestation":
    """Create a new Attestation with required fields."""
    return cls(
        attestor_id=attestor_id,
        attestation_timestamp=attestation_timestamp,
        proof_type=proof_type,
        proof_value=proof_value,
        verification_method_uri=verification_method_uri,
    )
with_anchor
with_anchor(uri: str) -> Attestation

Return a new Attestation with the specified anchor URI.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
87
88
89
90
91
92
93
94
95
96
97
def with_anchor(self, uri: str) -> "Attestation":
    """Return a new Attestation with the specified anchor URI."""
    return Attestation(
        attestor_id=self.attestor_id,
        attestation_timestamp=self.attestation_timestamp,
        proof_type=self.proof_type,
        proof_value=self.proof_value,
        verification_method_uri=self.verification_method_uri,
        proof_purpose=self.proof_purpose,
        anchor_uri=uri,
    )
with_purpose
with_purpose(purpose: ProofPurpose) -> Attestation

Return a new Attestation with the specified proof purpose.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
75
76
77
78
79
80
81
82
83
84
85
def with_purpose(self, purpose: ProofPurpose) -> "Attestation":
    """Return a new Attestation with the specified proof purpose."""
    return Attestation(
        attestor_id=self.attestor_id,
        attestation_timestamp=self.attestation_timestamp,
        proof_type=self.proof_type,
        proof_value=self.proof_value,
        verification_method_uri=self.verification_method_uri,
        proof_purpose=purpose,
        anchor_uri=self.anchor_uri,
    )
ProofPurpose

Bases: Enum

The purpose of a cryptographic proof.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
16
17
18
19
20
21
22
23
24
25
class ProofPurpose(Enum):
    """The purpose of a cryptographic proof."""

    ASSERTION_METHOD = "assertionMethod"
    AUTHENTICATION = "authentication"
    CAPABILITY_DELEGATION = "capabilityDelegation"

    def as_str(self) -> str:
        """Return the canonical string representation."""
        return self.value
as_str
as_str() -> str

Return the canonical string representation.

Source code in src/python/src/civic_exchange_protocol/core/attestation.py
23
24
25
def as_str(self) -> str:
    """Return the canonical string representation."""
    return self.value

canonical

Canonical serialization for CEP records.

This module provides the base class and utilities for generating deterministic canonical strings from CEP records. The canonical string is the input to SHA-256 hashing for record integrity verification.

Canonicalization Rules: 1. Field Order: Fields MUST be serialized in alphabetical order. 2. Null/Empty Omission: Fields with None or empty string values MUST be omitted entirely from the canonical string. 3. Timestamp Format: All timestamps MUST use YYYY-MM-DDTHH:MM:SS.ffffffZ with exactly 6 decimal places for microseconds. 4. Numeric Format: Monetary amounts MUST use exactly 2 decimal places. Integers MUST NOT have decimal points. 5. String Escaping: Strings are NOT JSON-escaped in the canonical form. The canonical string is a simple key:value concatenation. 6. Encoding: The canonical string MUST be UTF-8 encoded.

Canonicalize

Bases: ABC

Base class for types that can be serialized to a canonical string for hashing.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Canonicalize(ABC):
    """Base class for types that can be serialized to a canonical string for hashing."""

    @abstractmethod
    def canonical_fields(self) -> dict[str, str]:
        """Return the ordered map of field names to their canonical string values.

        Fields with None/null/empty values should NOT be included in the dict.
        The dict will be sorted alphabetically by key.

        Returns:
            A dictionary of field names to string values.
        """
        pass

    def to_canonical_string(self) -> str:
        """Generate the canonical string representation for hashing.

        Format: "field1":"value1","field2":"value2",...

        Fields are ordered alphabetically by key.

        Returns:
            The canonical string.
        """
        fields = self.canonical_fields()
        # Sort by key alphabetically
        sorted_items = sorted(fields.items(), key=lambda x: x[0])
        parts = [f'"{k}":"{v}"' for k, v in sorted_items]
        return ",".join(parts)

    def calculate_hash(self) -> CanonicalHash:
        """Compute the SHA-256 hash of the canonical string.

        Returns:
            A CanonicalHash instance.
        """
        return CanonicalHash.from_canonical_string(self.to_canonical_string())
calculate_hash
calculate_hash() -> CanonicalHash

Compute the SHA-256 hash of the canonical string.

Returns:

Type Description
CanonicalHash

A CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
57
58
59
60
61
62
63
def calculate_hash(self) -> CanonicalHash:
    """Compute the SHA-256 hash of the canonical string.

    Returns:
        A CanonicalHash instance.
    """
    return CanonicalHash.from_canonical_string(self.to_canonical_string())
canonical_fields abstractmethod
canonical_fields() -> dict[str, str]

Return the ordered map of field names to their canonical string values.

Fields with None/null/empty values should NOT be included in the dict. The dict will be sorted alphabetically by key.

Returns:

Type Description
dict[str, str]

A dictionary of field names to string values.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
29
30
31
32
33
34
35
36
37
38
39
@abstractmethod
def canonical_fields(self) -> dict[str, str]:
    """Return the ordered map of field names to their canonical string values.

    Fields with None/null/empty values should NOT be included in the dict.
    The dict will be sorted alphabetically by key.

    Returns:
        A dictionary of field names to string values.
    """
    pass
to_canonical_string
to_canonical_string() -> str

Generate the canonical string representation for hashing.

Format: "field1":"value1","field2":"value2",...

Fields are ordered alphabetically by key.

Returns:

Type Description
str

The canonical string.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
def to_canonical_string(self) -> str:
    """Generate the canonical string representation for hashing.

    Format: "field1":"value1","field2":"value2",...

    Fields are ordered alphabetically by key.

    Returns:
        The canonical string.
    """
    fields = self.canonical_fields()
    # Sort by key alphabetically
    sorted_items = sorted(fields.items(), key=lambda x: x[0])
    parts = [f'"{k}":"{v}"' for k, v in sorted_items]
    return ",".join(parts)
format_amount
format_amount(amount: float) -> str

Format a monetary amount with exactly 2 decimal places.

This ensures consistent formatting across all implementations: - 100 becomes "100.00" - 100.5 becomes "100.50" - 100.756 becomes "100.76" (rounded)

Parameters:

Name Type Description Default
amount float

The monetary amount.

required

Returns:

Type Description
str

The formatted string with exactly 2 decimal places.

Source code in src/python/src/civic_exchange_protocol/core/canonical.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def format_amount(amount: float) -> str:
    """Format a monetary amount with exactly 2 decimal places.

    This ensures consistent formatting across all implementations:
    - 100 becomes "100.00"
    - 100.5 becomes "100.50"
    - 100.756 becomes "100.76" (rounded)

    Args:
        amount: The monetary amount.

    Returns:
        The formatted string with exactly 2 decimal places.
    """
    # Use Decimal for precise rounding
    d = Decimal(str(amount))
    rounded = d.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
    return f"{rounded:.2f}"
insert_if_present
insert_if_present(
    fields: dict[str, str], key: str, value: str | None
) -> None

Add a field to the dict only if the value is not None and not empty.

Parameters:

Name Type Description Default
fields dict[str, str]

The dictionary to add to.

required
key str

The field name.

required
value str | None

The field value (may be None or empty).

required
Source code in src/python/src/civic_exchange_protocol/core/canonical.py
86
87
88
89
90
91
92
93
94
95
def insert_if_present(fields: dict[str, str], key: str, value: str | None) -> None:
    """Add a field to the dict only if the value is not None and not empty.

    Args:
        fields: The dictionary to add to.
        key: The field name.
        value: The field value (may be None or empty).
    """
    if value is not None and value != "":
        fields[key] = value
insert_required
insert_required(
    fields: dict[str, str], key: str, value: str
) -> None

Add a required field to the dict.

Parameters:

Name Type Description Default
fields dict[str, str]

The dictionary to add to.

required
key str

The field name.

required
value str

The field value.

required
Source code in src/python/src/civic_exchange_protocol/core/canonical.py
 98
 99
100
101
102
103
104
105
106
def insert_required(fields: dict[str, str], key: str, value: str) -> None:
    """Add a required field to the dict.

    Args:
        fields: The dictionary to add to.
        key: The field name.
        value: The field value.
    """
    fields[key] = value

error

Error types for CEP operations.

CepError

Bases: Exception

Base exception for CEP operations.

Source code in src/python/src/civic_exchange_protocol/core/error.py
4
5
6
7
class CepError(Exception):
    """Base exception for CEP operations."""

    pass
HashMismatchError

Bases: CepError

Hash verification failed.

Source code in src/python/src/civic_exchange_protocol/core/error.py
50
51
52
53
54
55
56
57
class HashMismatchError(CepError):
    """Hash verification failed."""

    def __init__(self, expected: str, actual: str) -> None:
        """Initialize with expected and actual hash values."""
        super().__init__(f"hash verification failed: expected {expected}, got {actual}")
        self.expected = expected
        self.actual = actual
__init__
__init__(expected: str, actual: str) -> None

Initialize with expected and actual hash values.

Source code in src/python/src/civic_exchange_protocol/core/error.py
53
54
55
56
57
def __init__(self, expected: str, actual: str) -> None:
    """Initialize with expected and actual hash values."""
    super().__init__(f"hash verification failed: expected {expected}, got {actual}")
    self.expected = expected
    self.actual = actual
InvalidHashError

Bases: CepError

Invalid hash format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
18
19
20
21
22
23
class InvalidHashError(CepError):
    """Invalid hash format."""

    def __init__(self, value: str) -> None:
        """Initialize with the invalid hash value."""
        super().__init__(f"invalid hash: expected 64 hex characters, got {value}")
__init__
__init__(value: str) -> None

Initialize with the invalid hash value.

Source code in src/python/src/civic_exchange_protocol/core/error.py
21
22
23
def __init__(self, value: str) -> None:
    """Initialize with the invalid hash value."""
    super().__init__(f"invalid hash: expected 64 hex characters, got {value}")
InvalidIdentifierError

Bases: CepError

Invalid identifier format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
26
27
28
29
30
31
class InvalidIdentifierError(CepError):
    """Invalid identifier format."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about invalid identifier."""
        super().__init__(f"invalid identifier: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about invalid identifier.

Source code in src/python/src/civic_exchange_protocol/core/error.py
29
30
31
def __init__(self, message: str) -> None:
    """Initialize with error message about invalid identifier."""
    super().__init__(f"invalid identifier: {message}")
InvalidTimestampError

Bases: CepError

Invalid timestamp format.

Source code in src/python/src/civic_exchange_protocol/core/error.py
10
11
12
13
14
15
class InvalidTimestampError(CepError):
    """Invalid timestamp format."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about invalid timestamp."""
        super().__init__(f"invalid timestamp: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about invalid timestamp.

Source code in src/python/src/civic_exchange_protocol/core/error.py
13
14
15
def __init__(self, message: str) -> None:
    """Initialize with error message about invalid timestamp."""
    super().__init__(f"invalid timestamp: {message}")
MissingFieldError

Bases: CepError

Missing required field.

Source code in src/python/src/civic_exchange_protocol/core/error.py
34
35
36
37
38
39
class MissingFieldError(CepError):
    """Missing required field."""

    def __init__(self, field: str) -> None:
        """Initialize with the name of the missing field."""
        super().__init__(f"missing required field: {field}")
__init__
__init__(field: str) -> None

Initialize with the name of the missing field.

Source code in src/python/src/civic_exchange_protocol/core/error.py
37
38
39
def __init__(self, field: str) -> None:
    """Initialize with the name of the missing field."""
    super().__init__(f"missing required field: {field}")
RevisionChainError

Bases: CepError

Revision chain error.

Source code in src/python/src/civic_exchange_protocol/core/error.py
60
61
62
63
64
65
class RevisionChainError(CepError):
    """Revision chain error."""

    def __init__(self, message: str) -> None:
        """Initialize with error message about revision chain."""
        super().__init__(f"revision chain error: {message}")
__init__
__init__(message: str) -> None

Initialize with error message about revision chain.

Source code in src/python/src/civic_exchange_protocol/core/error.py
63
64
65
def __init__(self, message: str) -> None:
    """Initialize with error message about revision chain."""
    super().__init__(f"revision chain error: {message}")
UnsupportedVersionError

Bases: CepError

Schema version mismatch.

Source code in src/python/src/civic_exchange_protocol/core/error.py
42
43
44
45
46
47
class UnsupportedVersionError(CepError):
    """Schema version mismatch."""

    def __init__(self, version: str) -> None:
        """Initialize with the unsupported version string."""
        super().__init__(f"unsupported schema version: {version}")
__init__
__init__(version: str) -> None

Initialize with the unsupported version string.

Source code in src/python/src/civic_exchange_protocol/core/error.py
45
46
47
def __init__(self, version: str) -> None:
    """Initialize with the unsupported version string."""
    super().__init__(f"unsupported schema version: {version}")

hash

Cryptographic hashing utilities for CEP records.

All CEP hashes are SHA-256, represented as lowercase hexadecimal strings.

CanonicalHash

A SHA-256 hash value represented as a 64-character lowercase hex string.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
class CanonicalHash:
    """A SHA-256 hash value represented as a 64-character lowercase hex string."""

    __slots__ = ("_hex",)

    def __init__(self, hex_value: str) -> None:
        """Create a CanonicalHash from a hex string.

        Args:
            hex_value: A 64-character hexadecimal string.

        Raises:
            ValueError: If the string is not valid.
        """
        if len(hex_value) != 64:
            raise ValueError(f"Hash must be 64 hex characters, got {len(hex_value)}")
        if not all(c in "0123456789abcdefABCDEF" for c in hex_value):
            raise ValueError("Hash must contain only hexadecimal characters")
        self._hex = hex_value.lower()

    @classmethod
    def from_canonical_string(cls, canonical: str) -> "CanonicalHash":
        """Compute the SHA-256 hash of the given canonical string.

        Args:
            canonical: The canonical string to hash.

        Returns:
            A CanonicalHash instance.
        """
        hasher = hashlib.sha256()
        hasher.update(canonical.encode("utf-8"))
        return cls(hasher.hexdigest())

    @classmethod
    def from_hex(cls, hex_value: str) -> Optional["CanonicalHash"]:
        """Create a CanonicalHash from a pre-computed hex string.

        Args:
            hex_value: A hexadecimal string.

        Returns:
            A CanonicalHash instance, or None if invalid.
        """
        try:
            return cls(hex_value)
        except ValueError:
            return None

    def as_hex(self) -> str:
        """Return the hash as a lowercase hex string."""
        return self._hex

    def as_bytes(self) -> bytes:
        """Return the hash as bytes (32 bytes)."""
        return bytes.fromhex(self._hex)

    def __str__(self) -> str:
        """Return a string representation of the hash."""
        return self._hex

    def __repr__(self) -> str:
        """Return a detailed string representation of the hash."""
        return f"CanonicalHash({self._hex!r})"

    def __eq__(self, other: object) -> bool:
        """Check equality with another CanonicalHash instance."""
        if isinstance(other, CanonicalHash):
            return self._hex == other._hex
        return NotImplemented

    def __hash__(self) -> int:
        """Return a hash value for the CanonicalHash instance."""
        return hash(self._hex)
__eq__
__eq__(other: object) -> bool

Check equality with another CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
75
76
77
78
79
def __eq__(self, other: object) -> bool:
    """Check equality with another CanonicalHash instance."""
    if isinstance(other, CanonicalHash):
        return self._hex == other._hex
    return NotImplemented
__hash__
__hash__() -> int

Return a hash value for the CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
81
82
83
def __hash__(self) -> int:
    """Return a hash value for the CanonicalHash instance."""
    return hash(self._hex)
__init__
__init__(hex_value: str) -> None

Create a CanonicalHash from a hex string.

Parameters:

Name Type Description Default
hex_value str

A 64-character hexadecimal string.

required

Raises:

Type Description
ValueError

If the string is not valid.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def __init__(self, hex_value: str) -> None:
    """Create a CanonicalHash from a hex string.

    Args:
        hex_value: A 64-character hexadecimal string.

    Raises:
        ValueError: If the string is not valid.
    """
    if len(hex_value) != 64:
        raise ValueError(f"Hash must be 64 hex characters, got {len(hex_value)}")
    if not all(c in "0123456789abcdefABCDEF" for c in hex_value):
        raise ValueError("Hash must contain only hexadecimal characters")
    self._hex = hex_value.lower()
__repr__
__repr__() -> str

Return a detailed string representation of the hash.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
71
72
73
def __repr__(self) -> str:
    """Return a detailed string representation of the hash."""
    return f"CanonicalHash({self._hex!r})"
__str__
__str__() -> str

Return a string representation of the hash.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
67
68
69
def __str__(self) -> str:
    """Return a string representation of the hash."""
    return self._hex
as_bytes
as_bytes() -> bytes

Return the hash as bytes (32 bytes).

Source code in src/python/src/civic_exchange_protocol/core/hash.py
63
64
65
def as_bytes(self) -> bytes:
    """Return the hash as bytes (32 bytes)."""
    return bytes.fromhex(self._hex)
as_hex
as_hex() -> str

Return the hash as a lowercase hex string.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
59
60
61
def as_hex(self) -> str:
    """Return the hash as a lowercase hex string."""
    return self._hex
from_canonical_string classmethod
from_canonical_string(canonical: str) -> CanonicalHash

Compute the SHA-256 hash of the given canonical string.

Parameters:

Name Type Description Default
canonical str

The canonical string to hash.

required

Returns:

Type Description
CanonicalHash

A CanonicalHash instance.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
30
31
32
33
34
35
36
37
38
39
40
41
42
@classmethod
def from_canonical_string(cls, canonical: str) -> "CanonicalHash":
    """Compute the SHA-256 hash of the given canonical string.

    Args:
        canonical: The canonical string to hash.

    Returns:
        A CanonicalHash instance.
    """
    hasher = hashlib.sha256()
    hasher.update(canonical.encode("utf-8"))
    return cls(hasher.hexdigest())
from_hex classmethod
from_hex(hex_value: str) -> Optional[CanonicalHash]

Create a CanonicalHash from a pre-computed hex string.

Parameters:

Name Type Description Default
hex_value str

A hexadecimal string.

required

Returns:

Type Description
Optional[CanonicalHash]

A CanonicalHash instance, or None if invalid.

Source code in src/python/src/civic_exchange_protocol/core/hash.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@classmethod
def from_hex(cls, hex_value: str) -> Optional["CanonicalHash"]:
    """Create a CanonicalHash from a pre-computed hex string.

    Args:
        hex_value: A hexadecimal string.

    Returns:
        A CanonicalHash instance, or None if invalid.
    """
    try:
        return cls(hex_value)
    except ValueError:
        return None

timestamp

Canonical timestamp handling for CEP records.

All CEP timestamps MUST be: - UTC timezone (indicated by 'Z' suffix) - ISO 8601 format - Microsecond precision (exactly 6 decimal places)

Example: 2025-11-28T14:30:00.000000Z

CanonicalTimestamp

A canonical CEP timestamp with mandatory microsecond precision.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
class CanonicalTimestamp:
    """A canonical CEP timestamp with mandatory microsecond precision."""

    __slots__ = ("_dt",)

    def __init__(self, dt: datetime) -> None:
        """Create a new CanonicalTimestamp from a datetime.

        Args:
            dt: A datetime object. If naive, assumed to be UTC.
                If aware, will be converted to UTC.
        """
        if dt.tzinfo is None:
            # Naive datetime - assume UTC
            self._dt = dt.replace(tzinfo=UTC)
        else:
            # Convert to UTC
            self._dt = dt.astimezone(UTC)

    @classmethod
    def now(cls) -> "CanonicalTimestamp":
        """Return the current UTC time as a CanonicalTimestamp."""
        return cls(datetime.now(UTC))

    @classmethod
    def parse(cls, s: str) -> "CanonicalTimestamp":
        """Parse an ISO 8601 timestamp string.

        Accepts formats:
        - 2025-11-28T14:30:00.123456Z
        - 2025-11-28T14:30:00.123456+00:00
        - 2025-11-28T14:30:00Z (will add .000000)

        Args:
            s: The timestamp string to parse.

        Returns:
            A CanonicalTimestamp instance.

        Raises:
            ValueError: If the string cannot be parsed.
        """
        # Handle Z suffix
        if s.endswith("Z"):
            s = s[:-1] + "+00:00"

        # Try parsing with microseconds
        try:
            dt = datetime.fromisoformat(s)
            return cls(dt)
        except ValueError:
            pass

        # Try without microseconds and add them
        try:
            # Remove timezone for parsing, then add back
            if "+" in s:
                base, tz = s.rsplit("+", 1)
                dt = datetime.fromisoformat(base)
                dt = dt.replace(tzinfo=UTC)
                return cls(dt)
            if s.count("-") > 2:  # Has negative offset
                base, tz = s.rsplit("-", 1)
                dt = datetime.fromisoformat(base)
                dt = dt.replace(tzinfo=UTC)
                return cls(dt)
        except ValueError:
            pass

        raise ValueError(f"Cannot parse timestamp: {s}")

    def as_datetime(self) -> datetime:
        """Return the underlying datetime object (UTC)."""
        return self._dt

    def to_canonical_string(self) -> str:
        """Return the canonical string representation.

        Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

        This format is REQUIRED for hash stability across all CEP implementations.
        """
        return self._dt.strftime(CANONICAL_FORMAT)

    def __str__(self) -> str:
        """Return the canonical string representation of the timestamp."""
        return self.to_canonical_string()

    def __repr__(self) -> str:
        """Return the developer-friendly representation of the timestamp."""
        return f"CanonicalTimestamp({self.to_canonical_string()!r})"

    def __eq__(self, other: object) -> bool:
        """Check equality with another CanonicalTimestamp."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt == other._dt
        return NotImplemented

    def __lt__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is less than another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt < other._dt
        return NotImplemented

    def __le__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is less than or equal to another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt <= other._dt
        return NotImplemented

    def __gt__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is greater than another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt > other._dt
        return NotImplemented

    def __ge__(self, other: "CanonicalTimestamp") -> bool:
        """Check if this timestamp is greater than or equal to another."""
        if isinstance(other, CanonicalTimestamp):
            return self._dt >= other._dt
        return NotImplemented

    def __hash__(self) -> int:
        """Return the hash of the timestamp."""
        return hash(self._dt)
__eq__
__eq__(other: object) -> bool

Check equality with another CanonicalTimestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
109
110
111
112
113
def __eq__(self, other: object) -> bool:
    """Check equality with another CanonicalTimestamp."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt == other._dt
    return NotImplemented
__ge__
__ge__(other: CanonicalTimestamp) -> bool

Check if this timestamp is greater than or equal to another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
133
134
135
136
137
def __ge__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is greater than or equal to another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt >= other._dt
    return NotImplemented
__gt__
__gt__(other: CanonicalTimestamp) -> bool

Check if this timestamp is greater than another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
127
128
129
130
131
def __gt__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is greater than another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt > other._dt
    return NotImplemented
__hash__
__hash__() -> int

Return the hash of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
139
140
141
def __hash__(self) -> int:
    """Return the hash of the timestamp."""
    return hash(self._dt)
__init__
__init__(dt: datetime) -> None

Create a new CanonicalTimestamp from a datetime.

Parameters:

Name Type Description Default
dt datetime

A datetime object. If naive, assumed to be UTC. If aware, will be converted to UTC.

required
Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
22
23
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, dt: datetime) -> None:
    """Create a new CanonicalTimestamp from a datetime.

    Args:
        dt: A datetime object. If naive, assumed to be UTC.
            If aware, will be converted to UTC.
    """
    if dt.tzinfo is None:
        # Naive datetime - assume UTC
        self._dt = dt.replace(tzinfo=UTC)
    else:
        # Convert to UTC
        self._dt = dt.astimezone(UTC)
__le__
__le__(other: CanonicalTimestamp) -> bool

Check if this timestamp is less than or equal to another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
121
122
123
124
125
def __le__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is less than or equal to another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt <= other._dt
    return NotImplemented
__lt__
__lt__(other: CanonicalTimestamp) -> bool

Check if this timestamp is less than another.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
115
116
117
118
119
def __lt__(self, other: "CanonicalTimestamp") -> bool:
    """Check if this timestamp is less than another."""
    if isinstance(other, CanonicalTimestamp):
        return self._dt < other._dt
    return NotImplemented
__repr__
__repr__() -> str

Return the developer-friendly representation of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
105
106
107
def __repr__(self) -> str:
    """Return the developer-friendly representation of the timestamp."""
    return f"CanonicalTimestamp({self.to_canonical_string()!r})"
__str__
__str__() -> str

Return the canonical string representation of the timestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
101
102
103
def __str__(self) -> str:
    """Return the canonical string representation of the timestamp."""
    return self.to_canonical_string()
as_datetime
as_datetime() -> datetime

Return the underlying datetime object (UTC).

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
88
89
90
def as_datetime(self) -> datetime:
    """Return the underlying datetime object (UTC)."""
    return self._dt
now classmethod
now() -> CanonicalTimestamp

Return the current UTC time as a CanonicalTimestamp.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
36
37
38
39
@classmethod
def now(cls) -> "CanonicalTimestamp":
    """Return the current UTC time as a CanonicalTimestamp."""
    return cls(datetime.now(UTC))
parse classmethod
parse(s: str) -> CanonicalTimestamp

Parse an ISO 8601 timestamp string.

Accepts formats: - 2025-11-28T14:30:00.123456Z - 2025-11-28T14:30:00.123456+00:00 - 2025-11-28T14:30:00Z (will add .000000)

Parameters:

Name Type Description Default
s str

The timestamp string to parse.

required

Returns:

Type Description
CanonicalTimestamp

A CanonicalTimestamp instance.

Raises:

Type Description
ValueError

If the string cannot be parsed.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@classmethod
def parse(cls, s: str) -> "CanonicalTimestamp":
    """Parse an ISO 8601 timestamp string.

    Accepts formats:
    - 2025-11-28T14:30:00.123456Z
    - 2025-11-28T14:30:00.123456+00:00
    - 2025-11-28T14:30:00Z (will add .000000)

    Args:
        s: The timestamp string to parse.

    Returns:
        A CanonicalTimestamp instance.

    Raises:
        ValueError: If the string cannot be parsed.
    """
    # Handle Z suffix
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"

    # Try parsing with microseconds
    try:
        dt = datetime.fromisoformat(s)
        return cls(dt)
    except ValueError:
        pass

    # Try without microseconds and add them
    try:
        # Remove timezone for parsing, then add back
        if "+" in s:
            base, tz = s.rsplit("+", 1)
            dt = datetime.fromisoformat(base)
            dt = dt.replace(tzinfo=UTC)
            return cls(dt)
        if s.count("-") > 2:  # Has negative offset
            base, tz = s.rsplit("-", 1)
            dt = datetime.fromisoformat(base)
            dt = dt.replace(tzinfo=UTC)
            return cls(dt)
    except ValueError:
        pass

    raise ValueError(f"Cannot parse timestamp: {s}")
to_canonical_string
to_canonical_string() -> str

Return the canonical string representation.

Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

This format is REQUIRED for hash stability across all CEP implementations.

Source code in src/python/src/civic_exchange_protocol/core/timestamp.py
92
93
94
95
96
97
98
99
def to_canonical_string(self) -> str:
    """Return the canonical string representation.

    Format: YYYY-MM-DDTHH:MM:SS.ffffffZ

    This format is REQUIRED for hash stability across all CEP implementations.
    """
    return self._dt.strftime(CANONICAL_FORMAT)

entity

CEP Entity - Entity records for the Civic Exchange Protocol.

This package defines the EntityRecord type, which represents a verified civic entity. Entities are the foundational primitive in CEP—all relationships and exchanges reference attested entities.

AdditionalScheme dataclass

An additional identifier scheme not explicitly defined in the schema.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
111
112
113
114
115
116
@dataclass
class AdditionalScheme:
    """An additional identifier scheme not explicitly defined in the schema."""

    scheme_uri: str
    value: str

CanadianBn dataclass

Canadian Business Number with program account.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@dataclass(frozen=True)
class CanadianBn:
    """Canadian Business Number with program account."""

    value: str

    def __post_init__(self) -> None:
        """Validate the Canadian BN format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid Canadian BN: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        # Pattern: 9 digits + 2 letters + 4 digits (e.g., 123456789RC0001)
        if len(value) != 15:
            return False
        digits1 = value[:9]
        letters = value[9:11]
        digits2 = value[11:15]
        return digits1.isdigit() and letters.isalpha() and letters.isupper() and digits2.isdigit()

    @classmethod
    def new(cls, value: str) -> Optional["CanadianBn"]:
        """Create a new Canadian BN, returning None if invalid."""
        try:
            return cls(value)
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the Canadian BN as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the Canadian BN format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
83
84
85
86
def __post_init__(self) -> None:
    """Validate the Canadian BN format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid Canadian BN: {self.value}")
as_str
as_str() -> str

Return the Canadian BN as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
106
107
108
def as_str(self) -> str:
    """Return the Canadian BN as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[CanadianBn]

Create a new Canadian BN, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
 98
 99
100
101
102
103
104
@classmethod
def new(cls, value: str) -> Optional["CanadianBn"]:
    """Create a new Canadian BN, returning None if invalid."""
    try:
        return cls(value)
    except ValueError:
        return None

EntityIdentifiers dataclass

Bases: Canonicalize

Collection of all known identifiers for an entity.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@dataclass
class EntityIdentifiers(Canonicalize):
    """Collection of all known identifiers for an entity."""

    sam_uei: SamUei | None = None
    lei: Lei | None = None
    snfei: Snfei | None = None
    canadian_bn: CanadianBn | None = None
    additional_schemes: list[AdditionalScheme] | None = None

    def with_sam_uei(self, uei: SamUei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the SAM UEI set."""
        return EntityIdentifiers(
            sam_uei=uei,
            lei=self.lei,
            snfei=self.snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def with_lei(self, lei: Lei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the LEI set."""
        return EntityIdentifiers(
            sam_uei=self.sam_uei,
            lei=lei,
            snfei=self.snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def with_snfei(self, snfei: Snfei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the SNFEI set."""
        return EntityIdentifiers(
            sam_uei=self.sam_uei,
            lei=self.lei,
            snfei=snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def has_any(self) -> bool:
        """Return True if at least one identifier is present."""
        return (
            self.sam_uei is not None
            or self.lei is not None
            or self.snfei is not None
            or self.canadian_bn is not None
            or (self.additional_schemes is not None and len(self.additional_schemes) > 0)
        )

    def primary_identifier(self) -> str | None:
        """Return the 'best' identifier for use as the verifiable ID.

        Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional
        """
        if self.lei is not None:
            return f"cep-entity:lei:{self.lei.as_str()}"
        if self.sam_uei is not None:
            return f"cep-entity:sam-uei:{self.sam_uei.as_str()}"
        if self.snfei is not None:
            return f"cep-entity:snfei:{self.snfei.as_str()}"
        if self.canadian_bn is not None:
            return f"cep-entity:canadian-bn:{self.canadian_bn.as_str()}"
        if self.additional_schemes and len(self.additional_schemes) > 0:
            return f"cep-entity:other:{self.additional_schemes[0].value}"
        return None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # Additional schemes serialized as JSON array string
        if self.additional_schemes and len(self.additional_schemes) > 0:
            sorted_schemes = sorted(self.additional_schemes, key=lambda x: x.scheme_uri)
            schemes_data = [{"schemeUri": s.scheme_uri, "value": s.value} for s in sorted_schemes]
            fields["additionalSchemes"] = json.dumps(schemes_data, separators=(",", ":"))

        insert_if_present(
            fields, "canadianBn", self.canadian_bn.as_str() if self.canadian_bn else None
        )
        insert_if_present(fields, "lei", self.lei.as_str() if self.lei else None)
        insert_if_present(fields, "samUei", self.sam_uei.as_str() if self.sam_uei else None)
        insert_if_present(fields, "snfei", self.snfei.as_str() if self.snfei else None)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # Additional schemes serialized as JSON array string
    if self.additional_schemes and len(self.additional_schemes) > 0:
        sorted_schemes = sorted(self.additional_schemes, key=lambda x: x.scheme_uri)
        schemes_data = [{"schemeUri": s.scheme_uri, "value": s.value} for s in sorted_schemes]
        fields["additionalSchemes"] = json.dumps(schemes_data, separators=(",", ":"))

    insert_if_present(
        fields, "canadianBn", self.canadian_bn.as_str() if self.canadian_bn else None
    )
    insert_if_present(fields, "lei", self.lei.as_str() if self.lei else None)
    insert_if_present(fields, "samUei", self.sam_uei.as_str() if self.sam_uei else None)
    insert_if_present(fields, "snfei", self.snfei.as_str() if self.snfei else None)

    return fields
has_any
has_any() -> bool

Return True if at least one identifier is present.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
159
160
161
162
163
164
165
166
167
def has_any(self) -> bool:
    """Return True if at least one identifier is present."""
    return (
        self.sam_uei is not None
        or self.lei is not None
        or self.snfei is not None
        or self.canadian_bn is not None
        or (self.additional_schemes is not None and len(self.additional_schemes) > 0)
    )
primary_identifier
primary_identifier() -> str | None

Return the 'best' identifier for use as the verifiable ID.

Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def primary_identifier(self) -> str | None:
    """Return the 'best' identifier for use as the verifiable ID.

    Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional
    """
    if self.lei is not None:
        return f"cep-entity:lei:{self.lei.as_str()}"
    if self.sam_uei is not None:
        return f"cep-entity:sam-uei:{self.sam_uei.as_str()}"
    if self.snfei is not None:
        return f"cep-entity:snfei:{self.snfei.as_str()}"
    if self.canadian_bn is not None:
        return f"cep-entity:canadian-bn:{self.canadian_bn.as_str()}"
    if self.additional_schemes and len(self.additional_schemes) > 0:
        return f"cep-entity:other:{self.additional_schemes[0].value}"
    return None
with_lei
with_lei(lei: Lei) -> EntityIdentifiers

Return a new EntityIdentifiers with the LEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
139
140
141
142
143
144
145
146
147
def with_lei(self, lei: Lei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the LEI set."""
    return EntityIdentifiers(
        sam_uei=self.sam_uei,
        lei=lei,
        snfei=self.snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )
with_sam_uei
with_sam_uei(uei: SamUei) -> EntityIdentifiers

Return a new EntityIdentifiers with the SAM UEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
129
130
131
132
133
134
135
136
137
def with_sam_uei(self, uei: SamUei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the SAM UEI set."""
    return EntityIdentifiers(
        sam_uei=uei,
        lei=self.lei,
        snfei=self.snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )
with_snfei
with_snfei(snfei: Snfei) -> EntityIdentifiers

Return a new EntityIdentifiers with the SNFEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
149
150
151
152
153
154
155
156
157
def with_snfei(self, snfei: Snfei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the SNFEI set."""
    return EntityIdentifiers(
        sam_uei=self.sam_uei,
        lei=self.lei,
        snfei=snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )

EntityRecord dataclass

Bases: Canonicalize

A complete CEP Entity Record.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@dataclass
class EntityRecord(Canonicalize):
    """A complete CEP Entity Record."""

    # Required fields
    verifiable_id: str
    identifiers: EntityIdentifiers
    legal_name: str
    jurisdiction_iso: str
    status: EntityStatus
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    legal_name_normalized: str | None = None
    entity_type_uri: str | None = None
    naics_code: str | None = None
    resolution_confidence: ResolutionConfidence | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new(
        cls,
        verifiable_id: str,
        identifiers: EntityIdentifiers,
        legal_name: str,
        jurisdiction_iso: str,
        status: EntityStatus,
        attestation: Attestation,
    ) -> "EntityRecord":
        """Create a new EntityRecord with required fields."""
        return cls(
            verifiable_id=verifiable_id,
            identifiers=identifiers,
            legal_name=legal_name,
            jurisdiction_iso=jurisdiction_iso,
            status=status,
            attestation=attestation,
        )

    def with_normalized_name(self, name: str) -> "EntityRecord":
        """Return a new EntityRecord with the normalized name set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=name,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_entity_type(self, uri: str) -> "EntityRecord":
        """Return a new EntityRecord with the entity type URI set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_naics(self, code: str) -> "EntityRecord":
        """Return a new EntityRecord with the NAICS code set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_resolution_confidence(self, confidence: ResolutionConfidence) -> "EntityRecord":
        """Return a new EntityRecord with resolution confidence set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "EntityRecord":
        """Return a new EntityRecord with the previous hash set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "EntityRecord":
        """Return a new EntityRecord with the revision number set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def validate(self) -> None:
        """Validate that the record has all required fields properly set.

        Raises:
            ValueError: If validation fails.
        """
        if self.schema_version != SCHEMA_VERSION:
            raise ValueError(f"Unsupported schema version: {self.schema_version}")
        if not self.verifiable_id:
            raise ValueError("verifiableId is required")
        if not self.identifiers.has_any():
            raise ValueError("At least one identifier is required")
        if not self.legal_name:
            raise ValueError("legalName is required")
        if not self.jurisdiction_iso:
            raise ValueError("jurisdictionIso is required")
        if self.revision_number < 1:
            raise ValueError("revisionNumber must be >= 1")

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())
        insert_if_present(fields, "entityTypeUri", self.entity_type_uri)

        # Identifiers is a nested object
        identifiers_canonical = self.identifiers.to_canonical_string()
        if identifiers_canonical:
            insert_required(fields, "identifiers", identifiers_canonical)

        insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
        insert_required(fields, "legalName", self.legal_name)
        insert_if_present(fields, "legalNameNormalized", self.legal_name_normalized)
        insert_if_present(fields, "naicsCode", self.naics_code)

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

        # Resolution confidence is a nested object
        if self.resolution_confidence is not None:
            insert_required(
                fields, "resolutionConfidence", self.resolution_confidence.to_canonical_string()
            )

        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Status is a nested object
        insert_required(fields, "status", self.status.to_canonical_string())

        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())
    insert_if_present(fields, "entityTypeUri", self.entity_type_uri)

    # Identifiers is a nested object
    identifiers_canonical = self.identifiers.to_canonical_string()
    if identifiers_canonical:
        insert_required(fields, "identifiers", identifiers_canonical)

    insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
    insert_required(fields, "legalName", self.legal_name)
    insert_if_present(fields, "legalNameNormalized", self.legal_name_normalized)
    insert_if_present(fields, "naicsCode", self.naics_code)

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

    # Resolution confidence is a nested object
    if self.resolution_confidence is not None:
        insert_required(
            fields, "resolutionConfidence", self.resolution_confidence.to_canonical_string()
        )

    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Status is a nested object
    insert_required(fields, "status", self.status.to_canonical_string())

    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new classmethod
new(
    verifiable_id: str,
    identifiers: EntityIdentifiers,
    legal_name: str,
    jurisdiction_iso: str,
    status: EntityStatus,
    attestation: Attestation,
) -> EntityRecord

Create a new EntityRecord with required fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@classmethod
def new(
    cls,
    verifiable_id: str,
    identifiers: EntityIdentifiers,
    legal_name: str,
    jurisdiction_iso: str,
    status: EntityStatus,
    attestation: Attestation,
) -> "EntityRecord":
    """Create a new EntityRecord with required fields."""
    return cls(
        verifiable_id=verifiable_id,
        identifiers=identifiers,
        legal_name=legal_name,
        jurisdiction_iso=jurisdiction_iso,
        status=status,
        attestation=attestation,
    )
validate
validate() -> None

Validate that the record has all required fields properly set.

Raises:

Type Description
ValueError

If validation fails.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def validate(self) -> None:
    """Validate that the record has all required fields properly set.

    Raises:
        ValueError: If validation fails.
    """
    if self.schema_version != SCHEMA_VERSION:
        raise ValueError(f"Unsupported schema version: {self.schema_version}")
    if not self.verifiable_id:
        raise ValueError("verifiableId is required")
    if not self.identifiers.has_any():
        raise ValueError("At least one identifier is required")
    if not self.legal_name:
        raise ValueError("legalName is required")
    if not self.jurisdiction_iso:
        raise ValueError("jurisdictionIso is required")
    if self.revision_number < 1:
        raise ValueError("revisionNumber must be >= 1")
with_entity_type
with_entity_type(uri: str) -> EntityRecord

Return a new EntityRecord with the entity type URI set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def with_entity_type(self, uri: str) -> "EntityRecord":
    """Return a new EntityRecord with the entity type URI set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_naics
with_naics(code: str) -> EntityRecord

Return a new EntityRecord with the NAICS code set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def with_naics(self, code: str) -> "EntityRecord":
    """Return a new EntityRecord with the NAICS code set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_normalized_name
with_normalized_name(name: str) -> EntityRecord

Return a new EntityRecord with the normalized name set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def with_normalized_name(self, name: str) -> "EntityRecord":
    """Return a new EntityRecord with the normalized name set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=name,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(hash_val: CanonicalHash) -> EntityRecord

Return a new EntityRecord with the previous hash set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def with_previous_hash(self, hash_val: CanonicalHash) -> "EntityRecord":
    """Return a new EntityRecord with the previous hash set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_resolution_confidence
with_resolution_confidence(
    confidence: ResolutionConfidence,
) -> EntityRecord

Return a new EntityRecord with resolution confidence set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def with_resolution_confidence(self, confidence: ResolutionConfidence) -> "EntityRecord":
    """Return a new EntityRecord with resolution confidence set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> EntityRecord

Return a new EntityRecord with the revision number set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def with_revision(self, revision: int) -> "EntityRecord":
    """Return a new EntityRecord with the revision number set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )

EntityStatus dataclass

Bases: Canonicalize

Entity status information.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass
class EntityStatus(Canonicalize):
    """Entity status information."""

    status_code: EntityStatusCode
    status_effective_date: str  # YYYY-MM-DD format
    status_termination_date: str | None = None
    successor_entity_id: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for entity status.

        Returns:
        dict[str, str]
            A dictionary containing the canonical representation of status fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(fields, "statusEffectiveDate", self.status_effective_date)
        insert_if_present(fields, "statusTerminationDate", self.status_termination_date)
        insert_if_present(fields, "successorEntityId", self.successor_entity_id)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for entity status.

dict[str, str] A dictionary containing the canonical representation of status fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
52
53
54
55
56
57
58
59
60
61
62
63
64
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for entity status.

    Returns:
    dict[str, str]
        A dictionary containing the canonical representation of status fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(fields, "statusEffectiveDate", self.status_effective_date)
    insert_if_present(fields, "statusTerminationDate", self.status_termination_date)
    insert_if_present(fields, "successorEntityId", self.successor_entity_id)
    return fields

EntityStatusCode

Bases: Enum

Entity operational status.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class EntityStatusCode(Enum):
    """Entity operational status."""

    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"
    SUSPENDED = "SUSPENDED"
    DISSOLVED = "DISSOLVED"
    MERGED = "MERGED"

    def as_str(self) -> str:
        """Return the string representation of the status code.

        Returns:
        -------
        str
            The status code value as a string.
        """
        return self.value
as_str
as_str() -> str

Return the string representation of the status code.

Returns:

str The status code value as a string.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
32
33
34
35
36
37
38
39
40
def as_str(self) -> str:
    """Return the string representation of the status code.

    Returns:
    -------
    str
        The status code value as a string.
    """
    return self.value

Lei dataclass

Legal Entity Identifier per ISO 17442 (20 alphanumeric characters).

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@dataclass(frozen=True)
class Lei:
    """Legal Entity Identifier per ISO 17442 (20 alphanumeric characters)."""

    value: str

    def __post_init__(self) -> None:
        """Validate the LEI format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid LEI: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        return len(value) == 20 and value.isalnum()

    @classmethod
    def new(cls, value: str) -> Optional["Lei"]:
        """Create a new LEI, returning None if invalid."""
        try:
            return cls(value.upper())
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the LEI as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the LEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
55
56
57
58
def __post_init__(self) -> None:
    """Validate the LEI format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid LEI: {self.value}")
as_str
as_str() -> str

Return the LEI as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
72
73
74
def as_str(self) -> str:
    """Return the LEI as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[Lei]

Create a new LEI, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
64
65
66
67
68
69
70
@classmethod
def new(cls, value: str) -> Optional["Lei"]:
    """Create a new LEI, returning None if invalid."""
    try:
        return cls(value.upper())
    except ValueError:
        return None

ResolutionConfidence dataclass

Bases: Canonicalize

Entity resolution confidence metadata.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass
class ResolutionConfidence(Canonicalize):
    """Entity resolution confidence metadata."""

    score: float  # 0.0 to 1.0
    method_uri: str | None = None
    source_record_count: int | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for resolution confidence.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical representation of resolution confidence fields.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "methodUri", self.method_uri)
        # Score formatted to 2 decimal places
        insert_required(fields, "score", f"{self.score:.2f}")
        if self.source_record_count is not None:
            insert_required(fields, "sourceRecordCount", str(self.source_record_count))
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for resolution confidence.

Returns:

dict[str, str] A dictionary containing the canonical representation of resolution confidence fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for resolution confidence.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical representation of resolution confidence fields.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "methodUri", self.method_uri)
    # Score formatted to 2 decimal places
    insert_required(fields, "score", f"{self.score:.2f}")
    if self.source_record_count is not None:
        insert_required(fields, "sourceRecordCount", str(self.source_record_count))
    return fields

SamUei dataclass

SAM.gov Unique Entity Identifier (12 alphanumeric characters).

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@dataclass(frozen=True)
class SamUei:
    """SAM.gov Unique Entity Identifier (12 alphanumeric characters)."""

    value: str

    def __post_init__(self) -> None:
        """Validate the SAM UEI format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid SAM UEI: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        return (
            len(value) == 12 and all(c.isupper() or c.isdigit() for c in value) and value.isalnum()
        )

    @classmethod
    def new(cls, value: str) -> Optional["SamUei"]:
        """Create a new SAM UEI, returning None if invalid."""
        try:
            return cls(value)
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the SAM UEI as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the SAM UEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
25
26
27
28
def __post_init__(self) -> None:
    """Validate the SAM UEI format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid SAM UEI: {self.value}")
as_str
as_str() -> str

Return the SAM UEI as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
44
45
46
def as_str(self) -> str:
    """Return the SAM UEI as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[SamUei]

Create a new SAM UEI, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
36
37
38
39
40
41
42
@classmethod
def new(cls, value: str) -> Optional["SamUei"]:
    """Create a new SAM UEI, returning None if invalid."""
    try:
        return cls(value)
    except ValueError:
        return None

Snfei dataclass

A validated SNFEI (64-character lowercase hex string).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass(frozen=True)
class Snfei:
    """A validated SNFEI (64-character lowercase hex string)."""

    value: str

    def __post_init__(self) -> None:
        """Validate SNFEI format after initialization."""
        if len(self.value) != 64:
            raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
        if not all(c in "0123456789abcdef" for c in self.value):
            raise ValueError("SNFEI must be lowercase hex")

    def __str__(self) -> str:
        """Return string representation of SNFEI."""
        return self.value

    def __repr__(self) -> str:
        """Return abbreviated representation of SNFEI."""
        return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"

    def as_str(self) -> str:
        """Return the hash value (for API compatibility)."""
        return self.value

    def short(self, length: int = 12) -> str:
        """Return a shortened version for display."""
        return self.value[:length]
__post_init__
__post_init__() -> None

Validate SNFEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
32
33
34
35
36
37
def __post_init__(self) -> None:
    """Validate SNFEI format after initialization."""
    if len(self.value) != 64:
        raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
    if not all(c in "0123456789abcdef" for c in self.value):
        raise ValueError("SNFEI must be lowercase hex")
__repr__
__repr__() -> str

Return abbreviated representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
43
44
45
def __repr__(self) -> str:
    """Return abbreviated representation of SNFEI."""
    return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"
__str__
__str__() -> str

Return string representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
39
40
41
def __str__(self) -> str:
    """Return string representation of SNFEI."""
    return self.value
as_str
as_str() -> str

Return the hash value (for API compatibility).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
47
48
49
def as_str(self) -> str:
    """Return the hash value (for API compatibility)."""
    return self.value
short
short(length: int = 12) -> str

Return a shortened version for display.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
51
52
53
def short(self, length: int = 12) -> str:
    """Return a shortened version for display."""
    return self.value[:length]

entity

CEP Entity Record definition.

The Entity Record is the foundational primitive in CEP. It represents a verified civic entity (government agency, contractor, nonprofit, individual). All relationships and exchanges reference attested entities.

EntityRecord dataclass

Bases: Canonicalize

A complete CEP Entity Record.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
@dataclass
class EntityRecord(Canonicalize):
    """A complete CEP Entity Record."""

    # Required fields
    verifiable_id: str
    identifiers: EntityIdentifiers
    legal_name: str
    jurisdiction_iso: str
    status: EntityStatus
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    legal_name_normalized: str | None = None
    entity_type_uri: str | None = None
    naics_code: str | None = None
    resolution_confidence: ResolutionConfidence | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new(
        cls,
        verifiable_id: str,
        identifiers: EntityIdentifiers,
        legal_name: str,
        jurisdiction_iso: str,
        status: EntityStatus,
        attestation: Attestation,
    ) -> "EntityRecord":
        """Create a new EntityRecord with required fields."""
        return cls(
            verifiable_id=verifiable_id,
            identifiers=identifiers,
            legal_name=legal_name,
            jurisdiction_iso=jurisdiction_iso,
            status=status,
            attestation=attestation,
        )

    def with_normalized_name(self, name: str) -> "EntityRecord":
        """Return a new EntityRecord with the normalized name set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=name,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_entity_type(self, uri: str) -> "EntityRecord":
        """Return a new EntityRecord with the entity type URI set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_naics(self, code: str) -> "EntityRecord":
        """Return a new EntityRecord with the NAICS code set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_resolution_confidence(self, confidence: ResolutionConfidence) -> "EntityRecord":
        """Return a new EntityRecord with resolution confidence set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "EntityRecord":
        """Return a new EntityRecord with the previous hash set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "EntityRecord":
        """Return a new EntityRecord with the revision number set."""
        return EntityRecord(
            verifiable_id=self.verifiable_id,
            identifiers=self.identifiers,
            legal_name=self.legal_name,
            jurisdiction_iso=self.jurisdiction_iso,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            legal_name_normalized=self.legal_name_normalized,
            entity_type_uri=self.entity_type_uri,
            naics_code=self.naics_code,
            resolution_confidence=self.resolution_confidence,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def validate(self) -> None:
        """Validate that the record has all required fields properly set.

        Raises:
            ValueError: If validation fails.
        """
        if self.schema_version != SCHEMA_VERSION:
            raise ValueError(f"Unsupported schema version: {self.schema_version}")
        if not self.verifiable_id:
            raise ValueError("verifiableId is required")
        if not self.identifiers.has_any():
            raise ValueError("At least one identifier is required")
        if not self.legal_name:
            raise ValueError("legalName is required")
        if not self.jurisdiction_iso:
            raise ValueError("jurisdictionIso is required")
        if self.revision_number < 1:
            raise ValueError("revisionNumber must be >= 1")

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())
        insert_if_present(fields, "entityTypeUri", self.entity_type_uri)

        # Identifiers is a nested object
        identifiers_canonical = self.identifiers.to_canonical_string()
        if identifiers_canonical:
            insert_required(fields, "identifiers", identifiers_canonical)

        insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
        insert_required(fields, "legalName", self.legal_name)
        insert_if_present(fields, "legalNameNormalized", self.legal_name_normalized)
        insert_if_present(fields, "naicsCode", self.naics_code)

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

        # Resolution confidence is a nested object
        if self.resolution_confidence is not None:
            insert_required(
                fields, "resolutionConfidence", self.resolution_confidence.to_canonical_string()
            )

        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Status is a nested object
        insert_required(fields, "status", self.status.to_canonical_string())

        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())
    insert_if_present(fields, "entityTypeUri", self.entity_type_uri)

    # Identifiers is a nested object
    identifiers_canonical = self.identifiers.to_canonical_string()
    if identifiers_canonical:
        insert_required(fields, "identifiers", identifiers_canonical)

    insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
    insert_required(fields, "legalName", self.legal_name)
    insert_if_present(fields, "legalNameNormalized", self.legal_name_normalized)
    insert_if_present(fields, "naicsCode", self.naics_code)

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

    # Resolution confidence is a nested object
    if self.resolution_confidence is not None:
        insert_required(
            fields, "resolutionConfidence", self.resolution_confidence.to_canonical_string()
        )

    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Status is a nested object
    insert_required(fields, "status", self.status.to_canonical_string())

    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new classmethod
new(
    verifiable_id: str,
    identifiers: EntityIdentifiers,
    legal_name: str,
    jurisdiction_iso: str,
    status: EntityStatus,
    attestation: Attestation,
) -> EntityRecord

Create a new EntityRecord with required fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
@classmethod
def new(
    cls,
    verifiable_id: str,
    identifiers: EntityIdentifiers,
    legal_name: str,
    jurisdiction_iso: str,
    status: EntityStatus,
    attestation: Attestation,
) -> "EntityRecord":
    """Create a new EntityRecord with required fields."""
    return cls(
        verifiable_id=verifiable_id,
        identifiers=identifiers,
        legal_name=legal_name,
        jurisdiction_iso=jurisdiction_iso,
        status=status,
        attestation=attestation,
    )
validate
validate() -> None

Validate that the record has all required fields properly set.

Raises:

Type Description
ValueError

If validation fails.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def validate(self) -> None:
    """Validate that the record has all required fields properly set.

    Raises:
        ValueError: If validation fails.
    """
    if self.schema_version != SCHEMA_VERSION:
        raise ValueError(f"Unsupported schema version: {self.schema_version}")
    if not self.verifiable_id:
        raise ValueError("verifiableId is required")
    if not self.identifiers.has_any():
        raise ValueError("At least one identifier is required")
    if not self.legal_name:
        raise ValueError("legalName is required")
    if not self.jurisdiction_iso:
        raise ValueError("jurisdictionIso is required")
    if self.revision_number < 1:
        raise ValueError("revisionNumber must be >= 1")
with_entity_type
with_entity_type(uri: str) -> EntityRecord

Return a new EntityRecord with the entity type URI set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def with_entity_type(self, uri: str) -> "EntityRecord":
    """Return a new EntityRecord with the entity type URI set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_naics
with_naics(code: str) -> EntityRecord

Return a new EntityRecord with the NAICS code set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
def with_naics(self, code: str) -> "EntityRecord":
    """Return a new EntityRecord with the NAICS code set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_normalized_name
with_normalized_name(name: str) -> EntityRecord

Return a new EntityRecord with the normalized name set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
def with_normalized_name(self, name: str) -> "EntityRecord":
    """Return a new EntityRecord with the normalized name set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=name,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(hash_val: CanonicalHash) -> EntityRecord

Return a new EntityRecord with the previous hash set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def with_previous_hash(self, hash_val: CanonicalHash) -> "EntityRecord":
    """Return a new EntityRecord with the previous hash set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_resolution_confidence
with_resolution_confidence(
    confidence: ResolutionConfidence,
) -> EntityRecord

Return a new EntityRecord with resolution confidence set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def with_resolution_confidence(self, confidence: ResolutionConfidence) -> "EntityRecord":
    """Return a new EntityRecord with resolution confidence set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> EntityRecord

Return a new EntityRecord with the revision number set.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
def with_revision(self, revision: int) -> "EntityRecord":
    """Return a new EntityRecord with the revision number set."""
    return EntityRecord(
        verifiable_id=self.verifiable_id,
        identifiers=self.identifiers,
        legal_name=self.legal_name,
        jurisdiction_iso=self.jurisdiction_iso,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        legal_name_normalized=self.legal_name_normalized,
        entity_type_uri=self.entity_type_uri,
        naics_code=self.naics_code,
        resolution_confidence=self.resolution_confidence,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )
EntityStatus dataclass

Bases: Canonicalize

Entity status information.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
@dataclass
class EntityStatus(Canonicalize):
    """Entity status information."""

    status_code: EntityStatusCode
    status_effective_date: str  # YYYY-MM-DD format
    status_termination_date: str | None = None
    successor_entity_id: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for entity status.

        Returns:
        dict[str, str]
            A dictionary containing the canonical representation of status fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(fields, "statusEffectiveDate", self.status_effective_date)
        insert_if_present(fields, "statusTerminationDate", self.status_termination_date)
        insert_if_present(fields, "successorEntityId", self.successor_entity_id)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for entity status.

dict[str, str] A dictionary containing the canonical representation of status fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
52
53
54
55
56
57
58
59
60
61
62
63
64
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for entity status.

    Returns:
    dict[str, str]
        A dictionary containing the canonical representation of status fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(fields, "statusEffectiveDate", self.status_effective_date)
    insert_if_present(fields, "statusTerminationDate", self.status_termination_date)
    insert_if_present(fields, "successorEntityId", self.successor_entity_id)
    return fields
EntityStatusCode

Bases: Enum

Entity operational status.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class EntityStatusCode(Enum):
    """Entity operational status."""

    ACTIVE = "ACTIVE"
    INACTIVE = "INACTIVE"
    SUSPENDED = "SUSPENDED"
    DISSOLVED = "DISSOLVED"
    MERGED = "MERGED"

    def as_str(self) -> str:
        """Return the string representation of the status code.

        Returns:
        -------
        str
            The status code value as a string.
        """
        return self.value
as_str
as_str() -> str

Return the string representation of the status code.

Returns:

str The status code value as a string.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
32
33
34
35
36
37
38
39
40
def as_str(self) -> str:
    """Return the string representation of the status code.

    Returns:
    -------
    str
        The status code value as a string.
    """
    return self.value
ResolutionConfidence dataclass

Bases: Canonicalize

Entity resolution confidence metadata.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass
class ResolutionConfidence(Canonicalize):
    """Entity resolution confidence metadata."""

    score: float  # 0.0 to 1.0
    method_uri: str | None = None
    source_record_count: int | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for resolution confidence.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical representation of resolution confidence fields.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "methodUri", self.method_uri)
        # Score formatted to 2 decimal places
        insert_required(fields, "score", f"{self.score:.2f}")
        if self.source_record_count is not None:
            insert_required(fields, "sourceRecordCount", str(self.source_record_count))
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for resolution confidence.

Returns:

dict[str, str] A dictionary containing the canonical representation of resolution confidence fields.

Source code in src/python/src/civic_exchange_protocol/entity/entity.py
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for resolution confidence.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical representation of resolution confidence fields.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "methodUri", self.method_uri)
    # Score formatted to 2 decimal places
    insert_required(fields, "score", f"{self.score:.2f}")
    if self.source_record_count is not None:
        insert_required(fields, "sourceRecordCount", str(self.source_record_count))
    return fields

identifiers

Entity identifier types for CEP.

CEP supports multiple identifier schemes organized into tiers:

  • Tier 1 (Global): LEI (Legal Entity Identifier)
  • Tier 2 (Federal): SAM.gov UEI
  • Tier 3 (Sub-National): SNFEI (generated hash-based identifier)
  • Extended: Canadian BN, UK Companies House, etc.
AdditionalScheme dataclass

An additional identifier scheme not explicitly defined in the schema.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
111
112
113
114
115
116
@dataclass
class AdditionalScheme:
    """An additional identifier scheme not explicitly defined in the schema."""

    scheme_uri: str
    value: str
CanadianBn dataclass

Canadian Business Number with program account.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
@dataclass(frozen=True)
class CanadianBn:
    """Canadian Business Number with program account."""

    value: str

    def __post_init__(self) -> None:
        """Validate the Canadian BN format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid Canadian BN: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        # Pattern: 9 digits + 2 letters + 4 digits (e.g., 123456789RC0001)
        if len(value) != 15:
            return False
        digits1 = value[:9]
        letters = value[9:11]
        digits2 = value[11:15]
        return digits1.isdigit() and letters.isalpha() and letters.isupper() and digits2.isdigit()

    @classmethod
    def new(cls, value: str) -> Optional["CanadianBn"]:
        """Create a new Canadian BN, returning None if invalid."""
        try:
            return cls(value)
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the Canadian BN as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the Canadian BN format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
83
84
85
86
def __post_init__(self) -> None:
    """Validate the Canadian BN format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid Canadian BN: {self.value}")
as_str
as_str() -> str

Return the Canadian BN as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
106
107
108
def as_str(self) -> str:
    """Return the Canadian BN as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[CanadianBn]

Create a new Canadian BN, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
 98
 99
100
101
102
103
104
@classmethod
def new(cls, value: str) -> Optional["CanadianBn"]:
    """Create a new Canadian BN, returning None if invalid."""
    try:
        return cls(value)
    except ValueError:
        return None
EntityIdentifiers dataclass

Bases: Canonicalize

Collection of all known identifiers for an entity.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
@dataclass
class EntityIdentifiers(Canonicalize):
    """Collection of all known identifiers for an entity."""

    sam_uei: SamUei | None = None
    lei: Lei | None = None
    snfei: Snfei | None = None
    canadian_bn: CanadianBn | None = None
    additional_schemes: list[AdditionalScheme] | None = None

    def with_sam_uei(self, uei: SamUei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the SAM UEI set."""
        return EntityIdentifiers(
            sam_uei=uei,
            lei=self.lei,
            snfei=self.snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def with_lei(self, lei: Lei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the LEI set."""
        return EntityIdentifiers(
            sam_uei=self.sam_uei,
            lei=lei,
            snfei=self.snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def with_snfei(self, snfei: Snfei) -> "EntityIdentifiers":
        """Return a new EntityIdentifiers with the SNFEI set."""
        return EntityIdentifiers(
            sam_uei=self.sam_uei,
            lei=self.lei,
            snfei=snfei,
            canadian_bn=self.canadian_bn,
            additional_schemes=self.additional_schemes,
        )

    def has_any(self) -> bool:
        """Return True if at least one identifier is present."""
        return (
            self.sam_uei is not None
            or self.lei is not None
            or self.snfei is not None
            or self.canadian_bn is not None
            or (self.additional_schemes is not None and len(self.additional_schemes) > 0)
        )

    def primary_identifier(self) -> str | None:
        """Return the 'best' identifier for use as the verifiable ID.

        Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional
        """
        if self.lei is not None:
            return f"cep-entity:lei:{self.lei.as_str()}"
        if self.sam_uei is not None:
            return f"cep-entity:sam-uei:{self.sam_uei.as_str()}"
        if self.snfei is not None:
            return f"cep-entity:snfei:{self.snfei.as_str()}"
        if self.canadian_bn is not None:
            return f"cep-entity:canadian-bn:{self.canadian_bn.as_str()}"
        if self.additional_schemes and len(self.additional_schemes) > 0:
            return f"cep-entity:other:{self.additional_schemes[0].value}"
        return None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # Additional schemes serialized as JSON array string
        if self.additional_schemes and len(self.additional_schemes) > 0:
            sorted_schemes = sorted(self.additional_schemes, key=lambda x: x.scheme_uri)
            schemes_data = [{"schemeUri": s.scheme_uri, "value": s.value} for s in sorted_schemes]
            fields["additionalSchemes"] = json.dumps(schemes_data, separators=(",", ":"))

        insert_if_present(
            fields, "canadianBn", self.canadian_bn.as_str() if self.canadian_bn else None
        )
        insert_if_present(fields, "lei", self.lei.as_str() if self.lei else None)
        insert_if_present(fields, "samUei", self.sam_uei.as_str() if self.sam_uei else None)
        insert_if_present(fields, "snfei", self.snfei.as_str() if self.snfei else None)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # Additional schemes serialized as JSON array string
    if self.additional_schemes and len(self.additional_schemes) > 0:
        sorted_schemes = sorted(self.additional_schemes, key=lambda x: x.scheme_uri)
        schemes_data = [{"schemeUri": s.scheme_uri, "value": s.value} for s in sorted_schemes]
        fields["additionalSchemes"] = json.dumps(schemes_data, separators=(",", ":"))

    insert_if_present(
        fields, "canadianBn", self.canadian_bn.as_str() if self.canadian_bn else None
    )
    insert_if_present(fields, "lei", self.lei.as_str() if self.lei else None)
    insert_if_present(fields, "samUei", self.sam_uei.as_str() if self.sam_uei else None)
    insert_if_present(fields, "snfei", self.snfei.as_str() if self.snfei else None)

    return fields
has_any
has_any() -> bool

Return True if at least one identifier is present.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
159
160
161
162
163
164
165
166
167
def has_any(self) -> bool:
    """Return True if at least one identifier is present."""
    return (
        self.sam_uei is not None
        or self.lei is not None
        or self.snfei is not None
        or self.canadian_bn is not None
        or (self.additional_schemes is not None and len(self.additional_schemes) > 0)
    )
primary_identifier
primary_identifier() -> str | None

Return the 'best' identifier for use as the verifiable ID.

Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
def primary_identifier(self) -> str | None:
    """Return the 'best' identifier for use as the verifiable ID.

    Priority: LEI > SAM UEI > SNFEI > Canadian BN > first additional
    """
    if self.lei is not None:
        return f"cep-entity:lei:{self.lei.as_str()}"
    if self.sam_uei is not None:
        return f"cep-entity:sam-uei:{self.sam_uei.as_str()}"
    if self.snfei is not None:
        return f"cep-entity:snfei:{self.snfei.as_str()}"
    if self.canadian_bn is not None:
        return f"cep-entity:canadian-bn:{self.canadian_bn.as_str()}"
    if self.additional_schemes and len(self.additional_schemes) > 0:
        return f"cep-entity:other:{self.additional_schemes[0].value}"
    return None
with_lei
with_lei(lei: Lei) -> EntityIdentifiers

Return a new EntityIdentifiers with the LEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
139
140
141
142
143
144
145
146
147
def with_lei(self, lei: Lei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the LEI set."""
    return EntityIdentifiers(
        sam_uei=self.sam_uei,
        lei=lei,
        snfei=self.snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )
with_sam_uei
with_sam_uei(uei: SamUei) -> EntityIdentifiers

Return a new EntityIdentifiers with the SAM UEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
129
130
131
132
133
134
135
136
137
def with_sam_uei(self, uei: SamUei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the SAM UEI set."""
    return EntityIdentifiers(
        sam_uei=uei,
        lei=self.lei,
        snfei=self.snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )
with_snfei
with_snfei(snfei: Snfei) -> EntityIdentifiers

Return a new EntityIdentifiers with the SNFEI set.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
149
150
151
152
153
154
155
156
157
def with_snfei(self, snfei: Snfei) -> "EntityIdentifiers":
    """Return a new EntityIdentifiers with the SNFEI set."""
    return EntityIdentifiers(
        sam_uei=self.sam_uei,
        lei=self.lei,
        snfei=snfei,
        canadian_bn=self.canadian_bn,
        additional_schemes=self.additional_schemes,
    )
Lei dataclass

Legal Entity Identifier per ISO 17442 (20 alphanumeric characters).

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@dataclass(frozen=True)
class Lei:
    """Legal Entity Identifier per ISO 17442 (20 alphanumeric characters)."""

    value: str

    def __post_init__(self) -> None:
        """Validate the LEI format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid LEI: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        return len(value) == 20 and value.isalnum()

    @classmethod
    def new(cls, value: str) -> Optional["Lei"]:
        """Create a new LEI, returning None if invalid."""
        try:
            return cls(value.upper())
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the LEI as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the LEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
55
56
57
58
def __post_init__(self) -> None:
    """Validate the LEI format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid LEI: {self.value}")
as_str
as_str() -> str

Return the LEI as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
72
73
74
def as_str(self) -> str:
    """Return the LEI as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[Lei]

Create a new LEI, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
64
65
66
67
68
69
70
@classmethod
def new(cls, value: str) -> Optional["Lei"]:
    """Create a new LEI, returning None if invalid."""
    try:
        return cls(value.upper())
    except ValueError:
        return None
SamUei dataclass

SAM.gov Unique Entity Identifier (12 alphanumeric characters).

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@dataclass(frozen=True)
class SamUei:
    """SAM.gov Unique Entity Identifier (12 alphanumeric characters)."""

    value: str

    def __post_init__(self) -> None:
        """Validate the SAM UEI format after initialization."""
        if not self._is_valid(self.value):
            raise ValueError(f"Invalid SAM UEI: {self.value}")

    @staticmethod
    def _is_valid(value: str) -> bool:
        return (
            len(value) == 12 and all(c.isupper() or c.isdigit() for c in value) and value.isalnum()
        )

    @classmethod
    def new(cls, value: str) -> Optional["SamUei"]:
        """Create a new SAM UEI, returning None if invalid."""
        try:
            return cls(value)
        except ValueError:
            return None

    def as_str(self) -> str:
        """Return the SAM UEI as a string."""
        return self.value
__post_init__
__post_init__() -> None

Validate the SAM UEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
25
26
27
28
def __post_init__(self) -> None:
    """Validate the SAM UEI format after initialization."""
    if not self._is_valid(self.value):
        raise ValueError(f"Invalid SAM UEI: {self.value}")
as_str
as_str() -> str

Return the SAM UEI as a string.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
44
45
46
def as_str(self) -> str:
    """Return the SAM UEI as a string."""
    return self.value
new classmethod
new(value: str) -> Optional[SamUei]

Create a new SAM UEI, returning None if invalid.

Source code in src/python/src/civic_exchange_protocol/entity/identifiers.py
36
37
38
39
40
41
42
@classmethod
def new(cls, value: str) -> Optional["SamUei"]:
    """Create a new SAM UEI, returning None if invalid."""
    try:
        return cls(value)
    except ValueError:
        return None

exchange

CEP Exchange - Exchange records for the Civic Exchange Protocol.

This package defines the ExchangeRecord type, which represents a verifiable value exchange (financial, in-kind, or informational) between entities within an established relationship. This is the atomic unit of civic transparency.

ExchangeCategorization dataclass

Bases: Canonicalize

Categorization codes for reporting and analysis.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
@dataclass
class ExchangeCategorization(Canonicalize):
    """Categorization codes for reporting and analysis."""

    cfda_number: str | None = None
    naics_code: str | None = None
    gtas_account_code: str | None = None
    local_category_code: str | None = None
    local_category_label: str | None = None

    def with_cfda(self, cfda: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with CFDA set."""
        return ExchangeCategorization(
            cfda_number=cfda,
            naics_code=self.naics_code,
            gtas_account_code=self.gtas_account_code,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_naics(self, naics: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with NAICS set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=naics,
            gtas_account_code=self.gtas_account_code,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_gtas(self, gtas: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with GTAS set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=self.naics_code,
            gtas_account_code=gtas,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_local_category(self, code: str, label: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with local category set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=self.naics_code,
            gtas_account_code=self.gtas_account_code,
            local_category_code=code,
            local_category_label=label,
        )

    def has_any(self) -> bool:
        """Return True if any categorization is present."""
        return (
            self.cfda_number is not None
            or self.naics_code is not None
            or self.gtas_account_code is not None
            or self.local_category_code is not None
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the exchange categorization.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "cfdaNumber", self.cfda_number)
        insert_if_present(fields, "gtasAccountCode", self.gtas_account_code)
        insert_if_present(fields, "localCategoryCode", self.local_category_code)
        insert_if_present(fields, "localCategoryLabel", self.local_category_label)
        insert_if_present(fields, "naicsCode", self.naics_code)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the exchange categorization.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the exchange categorization.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "cfdaNumber", self.cfda_number)
    insert_if_present(fields, "gtasAccountCode", self.gtas_account_code)
    insert_if_present(fields, "localCategoryCode", self.local_category_code)
    insert_if_present(fields, "localCategoryLabel", self.local_category_label)
    insert_if_present(fields, "naicsCode", self.naics_code)
    return fields
has_any
has_any() -> bool

Return True if any categorization is present.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
166
167
168
169
170
171
172
173
def has_any(self) -> bool:
    """Return True if any categorization is present."""
    return (
        self.cfda_number is not None
        or self.naics_code is not None
        or self.gtas_account_code is not None
        or self.local_category_code is not None
    )
with_cfda
with_cfda(cfda: str) -> ExchangeCategorization

Return a new ExchangeCategorization with CFDA set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
126
127
128
129
130
131
132
133
134
def with_cfda(self, cfda: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with CFDA set."""
    return ExchangeCategorization(
        cfda_number=cfda,
        naics_code=self.naics_code,
        gtas_account_code=self.gtas_account_code,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )
with_gtas
with_gtas(gtas: str) -> ExchangeCategorization

Return a new ExchangeCategorization with GTAS set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
146
147
148
149
150
151
152
153
154
def with_gtas(self, gtas: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with GTAS set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=self.naics_code,
        gtas_account_code=gtas,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )
with_local_category
with_local_category(
    code: str, label: str
) -> ExchangeCategorization

Return a new ExchangeCategorization with local category set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
156
157
158
159
160
161
162
163
164
def with_local_category(self, code: str, label: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with local category set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=self.naics_code,
        gtas_account_code=self.gtas_account_code,
        local_category_code=code,
        local_category_label=label,
    )
with_naics
with_naics(naics: str) -> ExchangeCategorization

Return a new ExchangeCategorization with NAICS set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
136
137
138
139
140
141
142
143
144
def with_naics(self, naics: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with NAICS set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=naics,
        gtas_account_code=self.gtas_account_code,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )

ExchangeParty dataclass

Bases: Canonicalize

A party in an exchange (source or recipient).

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@dataclass
class ExchangeParty(Canonicalize):
    """A party in an exchange (source or recipient)."""

    entity_id: str
    role_uri: str | None = None
    account_identifier: str | None = None

    def with_role(self, role_uri: str) -> "ExchangeParty":
        """Return a new ExchangeParty with role set."""
        return ExchangeParty(
            entity_id=self.entity_id,
            role_uri=role_uri,
            account_identifier=self.account_identifier,
        )

    def with_account(self, account: str) -> "ExchangeParty":
        """Return a new ExchangeParty with account identifier set."""
        return ExchangeParty(
            entity_id=self.entity_id,
            role_uri=self.role_uri,
            account_identifier=account,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of this party as a dictionary.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields for this exchange party.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "accountIdentifier", self.account_identifier)
        insert_required(fields, "entityId", self.entity_id)
        insert_if_present(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of this party as a dictionary.

Returns:

dict[str, str] A dictionary containing the canonical fields for this exchange party.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
117
118
119
120
121
122
123
124
125
126
127
128
129
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of this party as a dictionary.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields for this exchange party.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "accountIdentifier", self.account_identifier)
    insert_required(fields, "entityId", self.entity_id)
    insert_if_present(fields, "roleUri", self.role_uri)
    return fields
with_account
with_account(account: str) -> ExchangeParty

Return a new ExchangeParty with account identifier set.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
109
110
111
112
113
114
115
def with_account(self, account: str) -> "ExchangeParty":
    """Return a new ExchangeParty with account identifier set."""
    return ExchangeParty(
        entity_id=self.entity_id,
        role_uri=self.role_uri,
        account_identifier=account,
    )
with_role
with_role(role_uri: str) -> ExchangeParty

Return a new ExchangeParty with role set.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
101
102
103
104
105
106
107
def with_role(self, role_uri: str) -> "ExchangeParty":
    """Return a new ExchangeParty with role set."""
    return ExchangeParty(
        entity_id=self.entity_id,
        role_uri=role_uri,
        account_identifier=self.account_identifier,
    )

ExchangeRecord dataclass

Bases: Canonicalize

A complete CEP Exchange Record.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
@dataclass
class ExchangeRecord(Canonicalize):
    """A complete CEP Exchange Record."""

    # Required fields
    verifiable_id: str
    relationship_id: str
    exchange_type_uri: str
    source_entity: ExchangeParty
    recipient_entity: ExchangeParty
    value: ExchangeValue
    occurred_timestamp: CanonicalTimestamp
    status: ExchangeStatus
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    provenance_chain: ProvenanceChain | None = None
    categorization: ExchangeCategorization | None = None
    source_references: list[SourceReference] | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new(
        cls,
        verifiable_id: str,
        relationship_id: str,
        exchange_type_uri: str,
        source_entity: ExchangeParty,
        recipient_entity: ExchangeParty,
        value: ExchangeValue,
        occurred_timestamp: CanonicalTimestamp,
        status: ExchangeStatus,
        attestation: Attestation,
    ) -> "ExchangeRecord":
        """Create a new ExchangeRecord with required fields."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_id=relationship_id,
            exchange_type_uri=exchange_type_uri,
            source_entity=source_entity,
            recipient_entity=recipient_entity,
            value=value,
            occurred_timestamp=occurred_timestamp,
            status=status,
            attestation=attestation,
        )

    def with_provenance(self, chain: ProvenanceChain) -> "ExchangeRecord":
        """Return a new ExchangeRecord with provenance chain set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_categorization(self, cat: ExchangeCategorization) -> "ExchangeRecord":
        """Return a new ExchangeRecord with categorization set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=cat,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_source_reference(self, reference: SourceReference) -> "ExchangeRecord":
        """Return a new ExchangeRecord with a source reference added."""
        refs = list(self.source_references) if self.source_references else []
        refs.append(reference)
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=refs,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "ExchangeRecord":
        """Return a new ExchangeRecord with previous hash set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "ExchangeRecord":
        """Return a new ExchangeRecord with revision number set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())

        if self.categorization is not None and self.categorization.has_any():
            insert_required(fields, "categorization", self.categorization.to_canonical_string())

        insert_required(fields, "exchangeTypeUri", self.exchange_type_uri)
        insert_required(fields, "occurredTimestamp", self.occurred_timestamp.to_canonical_string())

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

        if self.provenance_chain is not None and self.provenance_chain.has_any():
            insert_required(fields, "provenanceChain", self.provenance_chain.to_canonical_string())

        insert_required(fields, "recipientEntity", self.recipient_entity.to_canonical_string())
        insert_required(fields, "relationshipId", self.relationship_id)
        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Source references sorted by sourceSystemUri then sourceRecordId
        if self.source_references:
            sorted_refs = sorted(
                self.source_references,
                key=lambda r: (r.source_system_uri, r.source_record_id),
            )
            refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
            fields["sourceReferences"] = f"[{refs_json}]"

        insert_required(fields, "sourceEntity", self.source_entity.to_canonical_string())
        insert_required(fields, "status", self.status.to_canonical_string())
        insert_required(fields, "value", self.value.to_canonical_string())
        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())

    if self.categorization is not None and self.categorization.has_any():
        insert_required(fields, "categorization", self.categorization.to_canonical_string())

    insert_required(fields, "exchangeTypeUri", self.exchange_type_uri)
    insert_required(fields, "occurredTimestamp", self.occurred_timestamp.to_canonical_string())

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

    if self.provenance_chain is not None and self.provenance_chain.has_any():
        insert_required(fields, "provenanceChain", self.provenance_chain.to_canonical_string())

    insert_required(fields, "recipientEntity", self.recipient_entity.to_canonical_string())
    insert_required(fields, "relationshipId", self.relationship_id)
    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Source references sorted by sourceSystemUri then sourceRecordId
    if self.source_references:
        sorted_refs = sorted(
            self.source_references,
            key=lambda r: (r.source_system_uri, r.source_record_id),
        )
        refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
        fields["sourceReferences"] = f"[{refs_json}]"

    insert_required(fields, "sourceEntity", self.source_entity.to_canonical_string())
    insert_required(fields, "status", self.status.to_canonical_string())
    insert_required(fields, "value", self.value.to_canonical_string())
    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new classmethod
new(
    verifiable_id: str,
    relationship_id: str,
    exchange_type_uri: str,
    source_entity: ExchangeParty,
    recipient_entity: ExchangeParty,
    value: ExchangeValue,
    occurred_timestamp: CanonicalTimestamp,
    status: ExchangeStatus,
    attestation: Attestation,
) -> ExchangeRecord

Create a new ExchangeRecord with required fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@classmethod
def new(
    cls,
    verifiable_id: str,
    relationship_id: str,
    exchange_type_uri: str,
    source_entity: ExchangeParty,
    recipient_entity: ExchangeParty,
    value: ExchangeValue,
    occurred_timestamp: CanonicalTimestamp,
    status: ExchangeStatus,
    attestation: Attestation,
) -> "ExchangeRecord":
    """Create a new ExchangeRecord with required fields."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_id=relationship_id,
        exchange_type_uri=exchange_type_uri,
        source_entity=source_entity,
        recipient_entity=recipient_entity,
        value=value,
        occurred_timestamp=occurred_timestamp,
        status=status,
        attestation=attestation,
    )
with_categorization
with_categorization(
    cat: ExchangeCategorization,
) -> ExchangeRecord

Return a new ExchangeRecord with categorization set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def with_categorization(self, cat: ExchangeCategorization) -> "ExchangeRecord":
    """Return a new ExchangeRecord with categorization set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=cat,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(
    hash_val: CanonicalHash,
) -> ExchangeRecord

Return a new ExchangeRecord with previous hash set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def with_previous_hash(self, hash_val: CanonicalHash) -> "ExchangeRecord":
    """Return a new ExchangeRecord with previous hash set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_provenance
with_provenance(chain: ProvenanceChain) -> ExchangeRecord

Return a new ExchangeRecord with provenance chain set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def with_provenance(self, chain: ProvenanceChain) -> "ExchangeRecord":
    """Return a new ExchangeRecord with provenance chain set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> ExchangeRecord

Return a new ExchangeRecord with revision number set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def with_revision(self, revision: int) -> "ExchangeRecord":
    """Return a new ExchangeRecord with revision number set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )
with_source_reference
with_source_reference(
    reference: SourceReference,
) -> ExchangeRecord

Return a new ExchangeRecord with a source reference added.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def with_source_reference(self, reference: SourceReference) -> "ExchangeRecord":
    """Return a new ExchangeRecord with a source reference added."""
    refs = list(self.source_references) if self.source_references else []
    refs.append(reference)
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=refs,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )

ExchangeStatus dataclass

Bases: Canonicalize

Exchange status information.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@dataclass
class ExchangeStatus(Canonicalize):
    """Exchange status information."""

    status_code: ExchangeStatusCode
    status_effective_timestamp: CanonicalTimestamp

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the exchange status.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(
            fields,
            "statusEffectiveTimestamp",
            self.status_effective_timestamp.to_canonical_string(),
        )
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the exchange status.

Returns:

dict[str, str] Dictionary containing the canonical fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the exchange status.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(
        fields,
        "statusEffectiveTimestamp",
        self.status_effective_timestamp.to_canonical_string(),
    )
    return fields

ExchangeStatusCode

Bases: Enum

Exchange operational status.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
25
26
27
28
29
30
31
32
33
34
35
36
class ExchangeStatusCode(Enum):
    """Exchange operational status."""

    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    REVERSED = "REVERSED"
    CANCELED = "CANCELED"
    DISPUTED = "DISPUTED"

    def as_str(self) -> str:
        """Return the string value of the exchange status code."""
        return self.value
as_str
as_str() -> str

Return the string value of the exchange status code.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
34
35
36
def as_str(self) -> str:
    """Return the string value of the exchange status code."""
    return self.value

ExchangeValue dataclass

Bases: Canonicalize

The value being exchanged.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@dataclass
class ExchangeValue(Canonicalize):
    """The value being exchanged."""

    amount: float
    currency_code: str = "USD"
    value_type_uri: str = DEFAULT_VALUE_TYPE_URI
    in_kind_description: str | None = None

    @classmethod
    def monetary(cls, amount: float, currency_code: str = "USD") -> "ExchangeValue":
        """Create a new monetary value."""
        return cls(amount=amount, currency_code=currency_code)

    @classmethod
    def usd(cls, amount: float) -> "ExchangeValue":
        """Create a new USD monetary value."""
        return cls.monetary(amount, "USD")

    @classmethod
    def in_kind(cls, amount: float, description: str) -> "ExchangeValue":
        """Create an in-kind value with description."""
        return cls(
            amount=amount,
            currency_code="USD",
            value_type_uri=ValueType.in_kind().type_uri,
            in_kind_description=description,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of this value as a dictionary.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields for this exchange value.
        """
        fields: dict[str, str] = {}
        # Amount formatted to exactly 2 decimal places
        insert_required(fields, "amount", format_amount(self.amount))
        insert_required(fields, "currencyCode", self.currency_code)
        insert_if_present(fields, "inKindDescription", self.in_kind_description)
        insert_required(fields, "valueTypeUri", self.value_type_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of this value as a dictionary.

Returns:

dict[str, str] A dictionary containing the canonical fields for this exchange value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of this value as a dictionary.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields for this exchange value.
    """
    fields: dict[str, str] = {}
    # Amount formatted to exactly 2 decimal places
    insert_required(fields, "amount", format_amount(self.amount))
    insert_required(fields, "currencyCode", self.currency_code)
    insert_if_present(fields, "inKindDescription", self.in_kind_description)
    insert_required(fields, "valueTypeUri", self.value_type_uri)
    return fields
in_kind classmethod
in_kind(amount: float, description: str) -> ExchangeValue

Create an in-kind value with description.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
66
67
68
69
70
71
72
73
74
@classmethod
def in_kind(cls, amount: float, description: str) -> "ExchangeValue":
    """Create an in-kind value with description."""
    return cls(
        amount=amount,
        currency_code="USD",
        value_type_uri=ValueType.in_kind().type_uri,
        in_kind_description=description,
    )
monetary classmethod
monetary(
    amount: float, currency_code: str = 'USD'
) -> ExchangeValue

Create a new monetary value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
56
57
58
59
@classmethod
def monetary(cls, amount: float, currency_code: str = "USD") -> "ExchangeValue":
    """Create a new monetary value."""
    return cls(amount=amount, currency_code=currency_code)
usd classmethod
usd(amount: float) -> ExchangeValue

Create a new USD monetary value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
61
62
63
64
@classmethod
def usd(cls, amount: float) -> "ExchangeValue":
    """Create a new USD monetary value."""
    return cls.monetary(amount, "USD")

IntermediaryEntity dataclass

Bases: Canonicalize

An intermediary entity in the funding chain.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@dataclass
class IntermediaryEntity(Canonicalize):
    """An intermediary entity in the funding chain."""

    entity_id: str
    role_uri: str | None = None

    def with_role(self, role_uri: str) -> "IntermediaryEntity":
        """Return a new IntermediaryEntity with role set."""
        return IntermediaryEntity(entity_id=self.entity_id, role_uri=role_uri)

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the intermediary entity.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        insert_if_present(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the intermediary entity.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
23
24
25
26
27
28
29
30
31
32
33
34
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the intermediary entity.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    insert_if_present(fields, "roleUri", self.role_uri)
    return fields
with_role
with_role(role_uri: str) -> IntermediaryEntity

Return a new IntermediaryEntity with role set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
19
20
21
def with_role(self, role_uri: str) -> "IntermediaryEntity":
    """Return a new IntermediaryEntity with role set."""
    return IntermediaryEntity(entity_id=self.entity_id, role_uri=role_uri)

ProvenanceChain dataclass

Bases: Canonicalize

Provenance chain tracing the flow of funds.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@dataclass
class ProvenanceChain(Canonicalize):
    """Provenance chain tracing the flow of funds."""

    funding_chain_tag: str | None = None
    ultimate_source_entity_id: str | None = None
    intermediary_entities: list[IntermediaryEntity] | None = None
    parent_exchange_id: str | None = None

    def with_funding_chain_tag(self, tag: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with funding chain tag set."""
        return ProvenanceChain(
            funding_chain_tag=tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_ultimate_source(self, entity_id: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with ultimate source set."""
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_intermediary(self, entity: IntermediaryEntity) -> "ProvenanceChain":
        """Return a new ProvenanceChain with an intermediary added."""
        entities = list(self.intermediary_entities) if self.intermediary_entities else []
        entities.append(entity)
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_parent_exchange(self, exchange_id: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with parent exchange set."""
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=exchange_id,
        )

    def has_any(self) -> bool:
        """Return True if any provenance information is present."""
        return (
            self.funding_chain_tag is not None
            or self.ultimate_source_entity_id is not None
            or (self.intermediary_entities is not None and len(self.intermediary_entities) > 0)
            or self.parent_exchange_id is not None
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the provenance chain.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}

        insert_if_present(fields, "fundingChainTag", self.funding_chain_tag)

        # Intermediary entities serialized as array
        if self.intermediary_entities:
            entities_json = ",".join(e.to_canonical_string() for e in self.intermediary_entities)
            fields["intermediaryEntities"] = f"[{entities_json}]"

        insert_if_present(fields, "parentExchangeId", self.parent_exchange_id)
        insert_if_present(fields, "ultimateSourceEntityId", self.ultimate_source_entity_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the provenance chain.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the provenance chain.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}

    insert_if_present(fields, "fundingChainTag", self.funding_chain_tag)

    # Intermediary entities serialized as array
    if self.intermediary_entities:
        entities_json = ",".join(e.to_canonical_string() for e in self.intermediary_entities)
        fields["intermediaryEntities"] = f"[{entities_json}]"

    insert_if_present(fields, "parentExchangeId", self.parent_exchange_id)
    insert_if_present(fields, "ultimateSourceEntityId", self.ultimate_source_entity_id)

    return fields
has_any
has_any() -> bool

Return True if any provenance information is present.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
84
85
86
87
88
89
90
91
def has_any(self) -> bool:
    """Return True if any provenance information is present."""
    return (
        self.funding_chain_tag is not None
        or self.ultimate_source_entity_id is not None
        or (self.intermediary_entities is not None and len(self.intermediary_entities) > 0)
        or self.parent_exchange_id is not None
    )
with_funding_chain_tag
with_funding_chain_tag(tag: str) -> ProvenanceChain

Return a new ProvenanceChain with funding chain tag set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
46
47
48
49
50
51
52
53
def with_funding_chain_tag(self, tag: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with funding chain tag set."""
    return ProvenanceChain(
        funding_chain_tag=tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=self.parent_exchange_id,
    )
with_intermediary
with_intermediary(
    entity: IntermediaryEntity,
) -> ProvenanceChain

Return a new ProvenanceChain with an intermediary added.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
64
65
66
67
68
69
70
71
72
73
def with_intermediary(self, entity: IntermediaryEntity) -> "ProvenanceChain":
    """Return a new ProvenanceChain with an intermediary added."""
    entities = list(self.intermediary_entities) if self.intermediary_entities else []
    entities.append(entity)
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=entities,
        parent_exchange_id=self.parent_exchange_id,
    )
with_parent_exchange
with_parent_exchange(exchange_id: str) -> ProvenanceChain

Return a new ProvenanceChain with parent exchange set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
75
76
77
78
79
80
81
82
def with_parent_exchange(self, exchange_id: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with parent exchange set."""
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=exchange_id,
    )
with_ultimate_source
with_ultimate_source(entity_id: str) -> ProvenanceChain

Return a new ProvenanceChain with ultimate source set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
55
56
57
58
59
60
61
62
def with_ultimate_source(self, entity_id: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with ultimate source set."""
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=self.parent_exchange_id,
    )

SourceReference dataclass

Bases: Canonicalize

Reference to an authoritative source record.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@dataclass
class SourceReference(Canonicalize):
    """Reference to an authoritative source record."""

    source_system_uri: str
    source_record_id: str
    source_url: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the source reference.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "sourceRecordId", self.source_record_id)
        insert_required(fields, "sourceSystemUri", self.source_system_uri)
        insert_if_present(fields, "sourceUrl", self.source_url)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the source reference.

Returns:

dict[str, str] Dictionary containing the canonical fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
72
73
74
75
76
77
78
79
80
81
82
83
84
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the source reference.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "sourceRecordId", self.source_record_id)
    insert_required(fields, "sourceSystemUri", self.source_system_uri)
    insert_if_present(fields, "sourceUrl", self.source_url)
    return fields

ValueType dataclass

The type of value being exchanged.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@dataclass
class ValueType:
    """The type of value being exchanged."""

    type_uri: str

    @classmethod
    def monetary(cls) -> "ValueType":
        """Return a ValueType for monetary exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#monetary"
        )

    @classmethod
    def in_kind(cls) -> "ValueType":
        """Return a ValueType for in-kind exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#in-kind"
        )

    @classmethod
    def service_hours(cls) -> "ValueType":
        """Return a ValueType for service hours exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#service-hours"
        )
in_kind classmethod
in_kind() -> ValueType

Return a ValueType for in-kind exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
29
30
31
32
33
34
@classmethod
def in_kind(cls) -> "ValueType":
    """Return a ValueType for in-kind exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#in-kind"
    )
monetary classmethod
monetary() -> ValueType

Return a ValueType for monetary exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
22
23
24
25
26
27
@classmethod
def monetary(cls) -> "ValueType":
    """Return a ValueType for monetary exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#monetary"
    )
service_hours classmethod
service_hours() -> ValueType

Return a ValueType for service hours exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
36
37
38
39
40
41
@classmethod
def service_hours(cls) -> "ValueType":
    """Return a ValueType for service hours exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#service-hours"
    )

exchange

CEP Exchange Record definition.

An Exchange Record represents a verifiable value exchange (financial, in-kind, or informational) between entities within an established relationship. This is the atomic unit of civic transparency.

ExchangeRecord dataclass

Bases: Canonicalize

A complete CEP Exchange Record.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
@dataclass
class ExchangeRecord(Canonicalize):
    """A complete CEP Exchange Record."""

    # Required fields
    verifiable_id: str
    relationship_id: str
    exchange_type_uri: str
    source_entity: ExchangeParty
    recipient_entity: ExchangeParty
    value: ExchangeValue
    occurred_timestamp: CanonicalTimestamp
    status: ExchangeStatus
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    provenance_chain: ProvenanceChain | None = None
    categorization: ExchangeCategorization | None = None
    source_references: list[SourceReference] | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new(
        cls,
        verifiable_id: str,
        relationship_id: str,
        exchange_type_uri: str,
        source_entity: ExchangeParty,
        recipient_entity: ExchangeParty,
        value: ExchangeValue,
        occurred_timestamp: CanonicalTimestamp,
        status: ExchangeStatus,
        attestation: Attestation,
    ) -> "ExchangeRecord":
        """Create a new ExchangeRecord with required fields."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_id=relationship_id,
            exchange_type_uri=exchange_type_uri,
            source_entity=source_entity,
            recipient_entity=recipient_entity,
            value=value,
            occurred_timestamp=occurred_timestamp,
            status=status,
            attestation=attestation,
        )

    def with_provenance(self, chain: ProvenanceChain) -> "ExchangeRecord":
        """Return a new ExchangeRecord with provenance chain set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_categorization(self, cat: ExchangeCategorization) -> "ExchangeRecord":
        """Return a new ExchangeRecord with categorization set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=cat,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_source_reference(self, reference: SourceReference) -> "ExchangeRecord":
        """Return a new ExchangeRecord with a source reference added."""
        refs = list(self.source_references) if self.source_references else []
        refs.append(reference)
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=refs,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "ExchangeRecord":
        """Return a new ExchangeRecord with previous hash set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "ExchangeRecord":
        """Return a new ExchangeRecord with revision number set."""
        return ExchangeRecord(
            verifiable_id=self.verifiable_id,
            relationship_id=self.relationship_id,
            exchange_type_uri=self.exchange_type_uri,
            source_entity=self.source_entity,
            recipient_entity=self.recipient_entity,
            value=self.value,
            occurred_timestamp=self.occurred_timestamp,
            status=self.status,
            attestation=self.attestation,
            schema_version=self.schema_version,
            provenance_chain=self.provenance_chain,
            categorization=self.categorization,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())

        if self.categorization is not None and self.categorization.has_any():
            insert_required(fields, "categorization", self.categorization.to_canonical_string())

        insert_required(fields, "exchangeTypeUri", self.exchange_type_uri)
        insert_required(fields, "occurredTimestamp", self.occurred_timestamp.to_canonical_string())

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

        if self.provenance_chain is not None and self.provenance_chain.has_any():
            insert_required(fields, "provenanceChain", self.provenance_chain.to_canonical_string())

        insert_required(fields, "recipientEntity", self.recipient_entity.to_canonical_string())
        insert_required(fields, "relationshipId", self.relationship_id)
        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Source references sorted by sourceSystemUri then sourceRecordId
        if self.source_references:
            sorted_refs = sorted(
                self.source_references,
                key=lambda r: (r.source_system_uri, r.source_record_id),
            )
            refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
            fields["sourceReferences"] = f"[{refs_json}]"

        insert_required(fields, "sourceEntity", self.source_entity.to_canonical_string())
        insert_required(fields, "status", self.status.to_canonical_string())
        insert_required(fields, "value", self.value.to_canonical_string())
        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())

    if self.categorization is not None and self.categorization.has_any():
        insert_required(fields, "categorization", self.categorization.to_canonical_string())

    insert_required(fields, "exchangeTypeUri", self.exchange_type_uri)
    insert_required(fields, "occurredTimestamp", self.occurred_timestamp.to_canonical_string())

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())

    if self.provenance_chain is not None and self.provenance_chain.has_any():
        insert_required(fields, "provenanceChain", self.provenance_chain.to_canonical_string())

    insert_required(fields, "recipientEntity", self.recipient_entity.to_canonical_string())
    insert_required(fields, "relationshipId", self.relationship_id)
    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Source references sorted by sourceSystemUri then sourceRecordId
    if self.source_references:
        sorted_refs = sorted(
            self.source_references,
            key=lambda r: (r.source_system_uri, r.source_record_id),
        )
        refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
        fields["sourceReferences"] = f"[{refs_json}]"

    insert_required(fields, "sourceEntity", self.source_entity.to_canonical_string())
    insert_required(fields, "status", self.status.to_canonical_string())
    insert_required(fields, "value", self.value.to_canonical_string())
    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new classmethod
new(
    verifiable_id: str,
    relationship_id: str,
    exchange_type_uri: str,
    source_entity: ExchangeParty,
    recipient_entity: ExchangeParty,
    value: ExchangeValue,
    occurred_timestamp: CanonicalTimestamp,
    status: ExchangeStatus,
    attestation: Attestation,
) -> ExchangeRecord

Create a new ExchangeRecord with required fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
@classmethod
def new(
    cls,
    verifiable_id: str,
    relationship_id: str,
    exchange_type_uri: str,
    source_entity: ExchangeParty,
    recipient_entity: ExchangeParty,
    value: ExchangeValue,
    occurred_timestamp: CanonicalTimestamp,
    status: ExchangeStatus,
    attestation: Attestation,
) -> "ExchangeRecord":
    """Create a new ExchangeRecord with required fields."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_id=relationship_id,
        exchange_type_uri=exchange_type_uri,
        source_entity=source_entity,
        recipient_entity=recipient_entity,
        value=value,
        occurred_timestamp=occurred_timestamp,
        status=status,
        attestation=attestation,
    )
with_categorization
with_categorization(
    cat: ExchangeCategorization,
) -> ExchangeRecord

Return a new ExchangeRecord with categorization set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def with_categorization(self, cat: ExchangeCategorization) -> "ExchangeRecord":
    """Return a new ExchangeRecord with categorization set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=cat,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(
    hash_val: CanonicalHash,
) -> ExchangeRecord

Return a new ExchangeRecord with previous hash set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
def with_previous_hash(self, hash_val: CanonicalHash) -> "ExchangeRecord":
    """Return a new ExchangeRecord with previous hash set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_provenance
with_provenance(chain: ProvenanceChain) -> ExchangeRecord

Return a new ExchangeRecord with provenance chain set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def with_provenance(self, chain: ProvenanceChain) -> "ExchangeRecord":
    """Return a new ExchangeRecord with provenance chain set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> ExchangeRecord

Return a new ExchangeRecord with revision number set.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
def with_revision(self, revision: int) -> "ExchangeRecord":
    """Return a new ExchangeRecord with revision number set."""
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )
with_source_reference
with_source_reference(
    reference: SourceReference,
) -> ExchangeRecord

Return a new ExchangeRecord with a source reference added.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
def with_source_reference(self, reference: SourceReference) -> "ExchangeRecord":
    """Return a new ExchangeRecord with a source reference added."""
    refs = list(self.source_references) if self.source_references else []
    refs.append(reference)
    return ExchangeRecord(
        verifiable_id=self.verifiable_id,
        relationship_id=self.relationship_id,
        exchange_type_uri=self.exchange_type_uri,
        source_entity=self.source_entity,
        recipient_entity=self.recipient_entity,
        value=self.value,
        occurred_timestamp=self.occurred_timestamp,
        status=self.status,
        attestation=self.attestation,
        schema_version=self.schema_version,
        provenance_chain=self.provenance_chain,
        categorization=self.categorization,
        source_references=refs,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
ExchangeStatus dataclass

Bases: Canonicalize

Exchange status information.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@dataclass
class ExchangeStatus(Canonicalize):
    """Exchange status information."""

    status_code: ExchangeStatusCode
    status_effective_timestamp: CanonicalTimestamp

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the exchange status.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(
            fields,
            "statusEffectiveTimestamp",
            self.status_effective_timestamp.to_canonical_string(),
        )
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the exchange status.

Returns:

dict[str, str] Dictionary containing the canonical fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the exchange status.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(
        fields,
        "statusEffectiveTimestamp",
        self.status_effective_timestamp.to_canonical_string(),
    )
    return fields
ExchangeStatusCode

Bases: Enum

Exchange operational status.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
25
26
27
28
29
30
31
32
33
34
35
36
class ExchangeStatusCode(Enum):
    """Exchange operational status."""

    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    REVERSED = "REVERSED"
    CANCELED = "CANCELED"
    DISPUTED = "DISPUTED"

    def as_str(self) -> str:
        """Return the string value of the exchange status code."""
        return self.value
as_str
as_str() -> str

Return the string value of the exchange status code.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
34
35
36
def as_str(self) -> str:
    """Return the string value of the exchange status code."""
    return self.value
SourceReference dataclass

Bases: Canonicalize

Reference to an authoritative source record.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
@dataclass
class SourceReference(Canonicalize):
    """Reference to an authoritative source record."""

    source_system_uri: str
    source_record_id: str
    source_url: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the source reference.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical fields.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "sourceRecordId", self.source_record_id)
        insert_required(fields, "sourceSystemUri", self.source_system_uri)
        insert_if_present(fields, "sourceUrl", self.source_url)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the source reference.

Returns:

dict[str, str] Dictionary containing the canonical fields.

Source code in src/python/src/civic_exchange_protocol/exchange/exchange.py
72
73
74
75
76
77
78
79
80
81
82
83
84
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the source reference.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical fields.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "sourceRecordId", self.source_record_id)
    insert_required(fields, "sourceSystemUri", self.source_system_uri)
    insert_if_present(fields, "sourceUrl", self.source_url)
    return fields

provenance

Provenance chain tracking for CEP exchanges.

Traces the compositional flow of funds through the civic graph. This is the Category Theory morphism path implementation.

ExchangeCategorization dataclass

Bases: Canonicalize

Categorization codes for reporting and analysis.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
@dataclass
class ExchangeCategorization(Canonicalize):
    """Categorization codes for reporting and analysis."""

    cfda_number: str | None = None
    naics_code: str | None = None
    gtas_account_code: str | None = None
    local_category_code: str | None = None
    local_category_label: str | None = None

    def with_cfda(self, cfda: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with CFDA set."""
        return ExchangeCategorization(
            cfda_number=cfda,
            naics_code=self.naics_code,
            gtas_account_code=self.gtas_account_code,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_naics(self, naics: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with NAICS set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=naics,
            gtas_account_code=self.gtas_account_code,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_gtas(self, gtas: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with GTAS set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=self.naics_code,
            gtas_account_code=gtas,
            local_category_code=self.local_category_code,
            local_category_label=self.local_category_label,
        )

    def with_local_category(self, code: str, label: str) -> "ExchangeCategorization":
        """Return a new ExchangeCategorization with local category set."""
        return ExchangeCategorization(
            cfda_number=self.cfda_number,
            naics_code=self.naics_code,
            gtas_account_code=self.gtas_account_code,
            local_category_code=code,
            local_category_label=label,
        )

    def has_any(self) -> bool:
        """Return True if any categorization is present."""
        return (
            self.cfda_number is not None
            or self.naics_code is not None
            or self.gtas_account_code is not None
            or self.local_category_code is not None
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the exchange categorization.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "cfdaNumber", self.cfda_number)
        insert_if_present(fields, "gtasAccountCode", self.gtas_account_code)
        insert_if_present(fields, "localCategoryCode", self.local_category_code)
        insert_if_present(fields, "localCategoryLabel", self.local_category_label)
        insert_if_present(fields, "naicsCode", self.naics_code)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the exchange categorization.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the exchange categorization.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "cfdaNumber", self.cfda_number)
    insert_if_present(fields, "gtasAccountCode", self.gtas_account_code)
    insert_if_present(fields, "localCategoryCode", self.local_category_code)
    insert_if_present(fields, "localCategoryLabel", self.local_category_label)
    insert_if_present(fields, "naicsCode", self.naics_code)
    return fields
has_any
has_any() -> bool

Return True if any categorization is present.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
166
167
168
169
170
171
172
173
def has_any(self) -> bool:
    """Return True if any categorization is present."""
    return (
        self.cfda_number is not None
        or self.naics_code is not None
        or self.gtas_account_code is not None
        or self.local_category_code is not None
    )
with_cfda
with_cfda(cfda: str) -> ExchangeCategorization

Return a new ExchangeCategorization with CFDA set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
126
127
128
129
130
131
132
133
134
def with_cfda(self, cfda: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with CFDA set."""
    return ExchangeCategorization(
        cfda_number=cfda,
        naics_code=self.naics_code,
        gtas_account_code=self.gtas_account_code,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )
with_gtas
with_gtas(gtas: str) -> ExchangeCategorization

Return a new ExchangeCategorization with GTAS set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
146
147
148
149
150
151
152
153
154
def with_gtas(self, gtas: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with GTAS set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=self.naics_code,
        gtas_account_code=gtas,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )
with_local_category
with_local_category(
    code: str, label: str
) -> ExchangeCategorization

Return a new ExchangeCategorization with local category set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
156
157
158
159
160
161
162
163
164
def with_local_category(self, code: str, label: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with local category set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=self.naics_code,
        gtas_account_code=self.gtas_account_code,
        local_category_code=code,
        local_category_label=label,
    )
with_naics
with_naics(naics: str) -> ExchangeCategorization

Return a new ExchangeCategorization with NAICS set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
136
137
138
139
140
141
142
143
144
def with_naics(self, naics: str) -> "ExchangeCategorization":
    """Return a new ExchangeCategorization with NAICS set."""
    return ExchangeCategorization(
        cfda_number=self.cfda_number,
        naics_code=naics,
        gtas_account_code=self.gtas_account_code,
        local_category_code=self.local_category_code,
        local_category_label=self.local_category_label,
    )
IntermediaryEntity dataclass

Bases: Canonicalize

An intermediary entity in the funding chain.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@dataclass
class IntermediaryEntity(Canonicalize):
    """An intermediary entity in the funding chain."""

    entity_id: str
    role_uri: str | None = None

    def with_role(self, role_uri: str) -> "IntermediaryEntity":
        """Return a new IntermediaryEntity with role set."""
        return IntermediaryEntity(entity_id=self.entity_id, role_uri=role_uri)

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the intermediary entity.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        insert_if_present(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the intermediary entity.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
23
24
25
26
27
28
29
30
31
32
33
34
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the intermediary entity.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    insert_if_present(fields, "roleUri", self.role_uri)
    return fields
with_role
with_role(role_uri: str) -> IntermediaryEntity

Return a new IntermediaryEntity with role set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
19
20
21
def with_role(self, role_uri: str) -> "IntermediaryEntity":
    """Return a new IntermediaryEntity with role set."""
    return IntermediaryEntity(entity_id=self.entity_id, role_uri=role_uri)
ProvenanceChain dataclass

Bases: Canonicalize

Provenance chain tracing the flow of funds.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
@dataclass
class ProvenanceChain(Canonicalize):
    """Provenance chain tracing the flow of funds."""

    funding_chain_tag: str | None = None
    ultimate_source_entity_id: str | None = None
    intermediary_entities: list[IntermediaryEntity] | None = None
    parent_exchange_id: str | None = None

    def with_funding_chain_tag(self, tag: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with funding chain tag set."""
        return ProvenanceChain(
            funding_chain_tag=tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_ultimate_source(self, entity_id: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with ultimate source set."""
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_intermediary(self, entity: IntermediaryEntity) -> "ProvenanceChain":
        """Return a new ProvenanceChain with an intermediary added."""
        entities = list(self.intermediary_entities) if self.intermediary_entities else []
        entities.append(entity)
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=entities,
            parent_exchange_id=self.parent_exchange_id,
        )

    def with_parent_exchange(self, exchange_id: str) -> "ProvenanceChain":
        """Return a new ProvenanceChain with parent exchange set."""
        return ProvenanceChain(
            funding_chain_tag=self.funding_chain_tag,
            ultimate_source_entity_id=self.ultimate_source_entity_id,
            intermediary_entities=self.intermediary_entities,
            parent_exchange_id=exchange_id,
        )

    def has_any(self) -> bool:
        """Return True if any provenance information is present."""
        return (
            self.funding_chain_tag is not None
            or self.ultimate_source_entity_id is not None
            or (self.intermediary_entities is not None and len(self.intermediary_entities) > 0)
            or self.parent_exchange_id is not None
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields for the provenance chain.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical field names and values.
        """
        fields: dict[str, str] = {}

        insert_if_present(fields, "fundingChainTag", self.funding_chain_tag)

        # Intermediary entities serialized as array
        if self.intermediary_entities:
            entities_json = ",".join(e.to_canonical_string() for e in self.intermediary_entities)
            fields["intermediaryEntities"] = f"[{entities_json}]"

        insert_if_present(fields, "parentExchangeId", self.parent_exchange_id)
        insert_if_present(fields, "ultimateSourceEntityId", self.ultimate_source_entity_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields for the provenance chain.

Returns:

dict[str, str] A dictionary containing the canonical field names and values.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields for the provenance chain.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical field names and values.
    """
    fields: dict[str, str] = {}

    insert_if_present(fields, "fundingChainTag", self.funding_chain_tag)

    # Intermediary entities serialized as array
    if self.intermediary_entities:
        entities_json = ",".join(e.to_canonical_string() for e in self.intermediary_entities)
        fields["intermediaryEntities"] = f"[{entities_json}]"

    insert_if_present(fields, "parentExchangeId", self.parent_exchange_id)
    insert_if_present(fields, "ultimateSourceEntityId", self.ultimate_source_entity_id)

    return fields
has_any
has_any() -> bool

Return True if any provenance information is present.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
84
85
86
87
88
89
90
91
def has_any(self) -> bool:
    """Return True if any provenance information is present."""
    return (
        self.funding_chain_tag is not None
        or self.ultimate_source_entity_id is not None
        or (self.intermediary_entities is not None and len(self.intermediary_entities) > 0)
        or self.parent_exchange_id is not None
    )
with_funding_chain_tag
with_funding_chain_tag(tag: str) -> ProvenanceChain

Return a new ProvenanceChain with funding chain tag set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
46
47
48
49
50
51
52
53
def with_funding_chain_tag(self, tag: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with funding chain tag set."""
    return ProvenanceChain(
        funding_chain_tag=tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=self.parent_exchange_id,
    )
with_intermediary
with_intermediary(
    entity: IntermediaryEntity,
) -> ProvenanceChain

Return a new ProvenanceChain with an intermediary added.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
64
65
66
67
68
69
70
71
72
73
def with_intermediary(self, entity: IntermediaryEntity) -> "ProvenanceChain":
    """Return a new ProvenanceChain with an intermediary added."""
    entities = list(self.intermediary_entities) if self.intermediary_entities else []
    entities.append(entity)
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=entities,
        parent_exchange_id=self.parent_exchange_id,
    )
with_parent_exchange
with_parent_exchange(exchange_id: str) -> ProvenanceChain

Return a new ProvenanceChain with parent exchange set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
75
76
77
78
79
80
81
82
def with_parent_exchange(self, exchange_id: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with parent exchange set."""
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=self.ultimate_source_entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=exchange_id,
    )
with_ultimate_source
with_ultimate_source(entity_id: str) -> ProvenanceChain

Return a new ProvenanceChain with ultimate source set.

Source code in src/python/src/civic_exchange_protocol/exchange/provenance.py
55
56
57
58
59
60
61
62
def with_ultimate_source(self, entity_id: str) -> "ProvenanceChain":
    """Return a new ProvenanceChain with ultimate source set."""
    return ProvenanceChain(
        funding_chain_tag=self.funding_chain_tag,
        ultimate_source_entity_id=entity_id,
        intermediary_entities=self.intermediary_entities,
        parent_exchange_id=self.parent_exchange_id,
    )

value

Value types for CEP exchanges.

Supports monetary values (with currency) and in-kind contributions.

ExchangeParty dataclass

Bases: Canonicalize

A party in an exchange (source or recipient).

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
@dataclass
class ExchangeParty(Canonicalize):
    """A party in an exchange (source or recipient)."""

    entity_id: str
    role_uri: str | None = None
    account_identifier: str | None = None

    def with_role(self, role_uri: str) -> "ExchangeParty":
        """Return a new ExchangeParty with role set."""
        return ExchangeParty(
            entity_id=self.entity_id,
            role_uri=role_uri,
            account_identifier=self.account_identifier,
        )

    def with_account(self, account: str) -> "ExchangeParty":
        """Return a new ExchangeParty with account identifier set."""
        return ExchangeParty(
            entity_id=self.entity_id,
            role_uri=self.role_uri,
            account_identifier=account,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of this party as a dictionary.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields for this exchange party.
        """
        fields: dict[str, str] = {}
        insert_if_present(fields, "accountIdentifier", self.account_identifier)
        insert_required(fields, "entityId", self.entity_id)
        insert_if_present(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of this party as a dictionary.

Returns:

dict[str, str] A dictionary containing the canonical fields for this exchange party.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
117
118
119
120
121
122
123
124
125
126
127
128
129
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of this party as a dictionary.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields for this exchange party.
    """
    fields: dict[str, str] = {}
    insert_if_present(fields, "accountIdentifier", self.account_identifier)
    insert_required(fields, "entityId", self.entity_id)
    insert_if_present(fields, "roleUri", self.role_uri)
    return fields
with_account
with_account(account: str) -> ExchangeParty

Return a new ExchangeParty with account identifier set.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
109
110
111
112
113
114
115
def with_account(self, account: str) -> "ExchangeParty":
    """Return a new ExchangeParty with account identifier set."""
    return ExchangeParty(
        entity_id=self.entity_id,
        role_uri=self.role_uri,
        account_identifier=account,
    )
with_role
with_role(role_uri: str) -> ExchangeParty

Return a new ExchangeParty with role set.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
101
102
103
104
105
106
107
def with_role(self, role_uri: str) -> "ExchangeParty":
    """Return a new ExchangeParty with role set."""
    return ExchangeParty(
        entity_id=self.entity_id,
        role_uri=role_uri,
        account_identifier=self.account_identifier,
    )
ExchangeValue dataclass

Bases: Canonicalize

The value being exchanged.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
@dataclass
class ExchangeValue(Canonicalize):
    """The value being exchanged."""

    amount: float
    currency_code: str = "USD"
    value_type_uri: str = DEFAULT_VALUE_TYPE_URI
    in_kind_description: str | None = None

    @classmethod
    def monetary(cls, amount: float, currency_code: str = "USD") -> "ExchangeValue":
        """Create a new monetary value."""
        return cls(amount=amount, currency_code=currency_code)

    @classmethod
    def usd(cls, amount: float) -> "ExchangeValue":
        """Create a new USD monetary value."""
        return cls.monetary(amount, "USD")

    @classmethod
    def in_kind(cls, amount: float, description: str) -> "ExchangeValue":
        """Create an in-kind value with description."""
        return cls(
            amount=amount,
            currency_code="USD",
            value_type_uri=ValueType.in_kind().type_uri,
            in_kind_description=description,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of this value as a dictionary.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields for this exchange value.
        """
        fields: dict[str, str] = {}
        # Amount formatted to exactly 2 decimal places
        insert_required(fields, "amount", format_amount(self.amount))
        insert_required(fields, "currencyCode", self.currency_code)
        insert_if_present(fields, "inKindDescription", self.in_kind_description)
        insert_required(fields, "valueTypeUri", self.value_type_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of this value as a dictionary.

Returns:

dict[str, str] A dictionary containing the canonical fields for this exchange value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of this value as a dictionary.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields for this exchange value.
    """
    fields: dict[str, str] = {}
    # Amount formatted to exactly 2 decimal places
    insert_required(fields, "amount", format_amount(self.amount))
    insert_required(fields, "currencyCode", self.currency_code)
    insert_if_present(fields, "inKindDescription", self.in_kind_description)
    insert_required(fields, "valueTypeUri", self.value_type_uri)
    return fields
in_kind classmethod
in_kind(amount: float, description: str) -> ExchangeValue

Create an in-kind value with description.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
66
67
68
69
70
71
72
73
74
@classmethod
def in_kind(cls, amount: float, description: str) -> "ExchangeValue":
    """Create an in-kind value with description."""
    return cls(
        amount=amount,
        currency_code="USD",
        value_type_uri=ValueType.in_kind().type_uri,
        in_kind_description=description,
    )
monetary classmethod
monetary(
    amount: float, currency_code: str = 'USD'
) -> ExchangeValue

Create a new monetary value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
56
57
58
59
@classmethod
def monetary(cls, amount: float, currency_code: str = "USD") -> "ExchangeValue":
    """Create a new monetary value."""
    return cls(amount=amount, currency_code=currency_code)
usd classmethod
usd(amount: float) -> ExchangeValue

Create a new USD monetary value.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
61
62
63
64
@classmethod
def usd(cls, amount: float) -> "ExchangeValue":
    """Create a new USD monetary value."""
    return cls.monetary(amount, "USD")
ValueType dataclass

The type of value being exchanged.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@dataclass
class ValueType:
    """The type of value being exchanged."""

    type_uri: str

    @classmethod
    def monetary(cls) -> "ValueType":
        """Return a ValueType for monetary exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#monetary"
        )

    @classmethod
    def in_kind(cls) -> "ValueType":
        """Return a ValueType for in-kind exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#in-kind"
        )

    @classmethod
    def service_hours(cls) -> "ValueType":
        """Return a ValueType for service hours exchanges."""
        return cls(
            "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#service-hours"
        )
in_kind classmethod
in_kind() -> ValueType

Return a ValueType for in-kind exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
29
30
31
32
33
34
@classmethod
def in_kind(cls) -> "ValueType":
    """Return a ValueType for in-kind exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#in-kind"
    )
monetary classmethod
monetary() -> ValueType

Return a ValueType for monetary exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
22
23
24
25
26
27
@classmethod
def monetary(cls) -> "ValueType":
    """Return a ValueType for monetary exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#monetary"
    )
service_hours classmethod
service_hours() -> ValueType

Return a ValueType for service hours exchanges.

Source code in src/python/src/civic_exchange_protocol/exchange/value.py
36
37
38
39
40
41
@classmethod
def service_hours(cls) -> "ValueType":
    """Return a ValueType for service hours exchanges."""
    return cls(
        "https://raw.githubusercontent.com/civic-interconnect/civic-exchange-protocol/main/vocabulary/value-type.json#service-hours"
    )

relationship

CEP Relationship - Relationship records for the Civic Exchange Protocol.

This package defines the RelationshipRecord type, which represents a verifiable legal or functional relationship between two or more attested entities.

BilateralParties dataclass

Bases: Canonicalize

Bilateral parties in a two-party relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass
class BilateralParties(Canonicalize):
    """Bilateral parties in a two-party relationship."""

    party_a: Party  # Initiating, granting, or contracting party
    party_b: Party  # Receiving, performing, or beneficiary party

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of bilateral parties.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields with partyA and partyB.
        """
        fields: dict[str, str] = {}
        # Nested objects serialized as their canonical strings
        insert_required(fields, "partyA", self.party_a.to_canonical_string())
        insert_required(fields, "partyB", self.party_b.to_canonical_string())
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of bilateral parties.

Returns:

dict[str, str] A dictionary containing the canonical fields with partyA and partyB.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
41
42
43
44
45
46
47
48
49
50
51
52
53
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of bilateral parties.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields with partyA and partyB.
    """
    fields: dict[str, str] = {}
    # Nested objects serialized as their canonical strings
    insert_required(fields, "partyA", self.party_a.to_canonical_string())
    insert_required(fields, "partyB", self.party_b.to_canonical_string())
    return fields

FinancialTerms dataclass

Bases: Canonicalize

Financial terms of a relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@dataclass
class FinancialTerms(Canonicalize):
    """Financial terms of a relationship."""

    total_value: float | None = None
    obligated_value: float | None = None
    currency_code: str = "USD"

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of financial terms fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "currencyCode", self.currency_code)
        if self.obligated_value is not None:
            insert_required(fields, "obligatedValue", format_amount(self.obligated_value))
        if self.total_value is not None:
            insert_required(fields, "totalValue", format_amount(self.total_value))
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of financial terms fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of financial terms fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "currencyCode", self.currency_code)
    if self.obligated_value is not None:
        insert_required(fields, "obligatedValue", format_amount(self.obligated_value))
    if self.total_value is not None:
        insert_required(fields, "totalValue", format_amount(self.total_value))
    return fields

Member dataclass

Bases: Canonicalize

A member in a multilateral relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass
class Member(Canonicalize):
    """A member in a multilateral relationship."""

    entity_id: str
    role_uri: str
    participation_share: float | None = None

    def with_share(self, share: float) -> "Member":
        """Return a new Member with the participation share set."""
        return Member(
            entity_id=self.entity_id,
            role_uri=self.role_uri,
            participation_share=share,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the member.

        Returns:
        -------
        dict[str, str]
            Dictionary containing entityId, roleUri, and optionally participationShare.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        if self.participation_share is not None:
            insert_required(fields, "participationShare", f"{self.participation_share:.4f}")
        insert_required(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the member.

Returns:

dict[str, str] Dictionary containing entityId, roleUri, and optionally participationShare.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the member.

    Returns:
    -------
    dict[str, str]
        Dictionary containing entityId, roleUri, and optionally participationShare.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    if self.participation_share is not None:
        insert_required(fields, "participationShare", f"{self.participation_share:.4f}")
    insert_required(fields, "roleUri", self.role_uri)
    return fields
with_share
with_share(share: float) -> Member

Return a new Member with the participation share set.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
26
27
28
29
30
31
32
def with_share(self, share: float) -> "Member":
    """Return a new Member with the participation share set."""
    return Member(
        entity_id=self.entity_id,
        role_uri=self.role_uri,
        participation_share=share,
    )

MultilateralMembers

Bases: Canonicalize

A collection of members in a multilateral relationship.

Members are automatically sorted by entity_id to ensure hash stability regardless of insertion order.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class MultilateralMembers(Canonicalize):
    """A collection of members in a multilateral relationship.

    Members are automatically sorted by entity_id to ensure
    hash stability regardless of insertion order.
    """

    def __init__(self) -> None:
        """Initialize an empty collection of multilateral relationship members."""
        self._members: list[Member] = []

    def add(self, member: Member) -> None:
        """Add a member to the set."""
        # Check for duplicate entity_id
        for existing in self._members:
            if existing.entity_id == member.entity_id:
                return  # Already exists
        self._members.append(member)

    def __len__(self) -> int:
        """Return the number of members in the collection."""
        return len(self._members)

    def __iter__(self) -> Iterator[Member]:
        """Iterate over members in sorted order by entity_id."""
        return iter(sorted(self._members, key=lambda m: m.entity_id))

    def is_empty(self) -> bool:
        """Check if the collection has no members.

        Returns:
        -------
        bool
            True if the collection is empty, False otherwise.
        """
        return len(self._members) == 0

    def validate_shares(self) -> None:
        """Validate that all participation shares sum to 1.0 (if present).

        Raises:
            ValueError: If validation fails.
        """
        shares = [m.participation_share for m in self._members if m.participation_share is not None]

        if not shares:
            return

        if len(shares) != len(self._members):
            raise ValueError("All members must have participation shares if any do")

        total = sum(shares)
        if abs(total - 1.0) > 0.0001:
            raise ValueError(f"Participation shares must sum to 1.0, got {total:.4f}")

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields."""
        fields: dict[str, str] = {}

        # Serialize as array, members sorted by entity_id
        if self._members:
            sorted_members = sorted(self._members, key=lambda m: m.entity_id)
            members_json = ",".join(m.to_canonical_string() for m in sorted_members)
            fields["members"] = f"[{members_json}]"

        return fields
__init__
__init__() -> None

Initialize an empty collection of multilateral relationship members.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
57
58
59
def __init__(self) -> None:
    """Initialize an empty collection of multilateral relationship members."""
    self._members: list[Member] = []
__iter__
__iter__() -> Iterator[Member]

Iterate over members in sorted order by entity_id.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
73
74
75
def __iter__(self) -> Iterator[Member]:
    """Iterate over members in sorted order by entity_id."""
    return iter(sorted(self._members, key=lambda m: m.entity_id))
__len__
__len__() -> int

Return the number of members in the collection.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
69
70
71
def __len__(self) -> int:
    """Return the number of members in the collection."""
    return len(self._members)
add
add(member: Member) -> None

Add a member to the set.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
61
62
63
64
65
66
67
def add(self, member: Member) -> None:
    """Add a member to the set."""
    # Check for duplicate entity_id
    for existing in self._members:
        if existing.entity_id == member.entity_id:
            return  # Already exists
    self._members.append(member)
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
105
106
107
108
109
110
111
112
113
114
115
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields."""
    fields: dict[str, str] = {}

    # Serialize as array, members sorted by entity_id
    if self._members:
        sorted_members = sorted(self._members, key=lambda m: m.entity_id)
        members_json = ",".join(m.to_canonical_string() for m in sorted_members)
        fields["members"] = f"[{members_json}]"

    return fields
is_empty
is_empty() -> bool

Check if the collection has no members.

Returns:

bool True if the collection is empty, False otherwise.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
77
78
79
80
81
82
83
84
85
def is_empty(self) -> bool:
    """Check if the collection has no members.

    Returns:
    -------
    bool
        True if the collection is empty, False otherwise.
    """
    return len(self._members) == 0
validate_shares
validate_shares() -> None

Validate that all participation shares sum to 1.0 (if present).

Raises:

Type Description
ValueError

If validation fails.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def validate_shares(self) -> None:
    """Validate that all participation shares sum to 1.0 (if present).

    Raises:
        ValueError: If validation fails.
    """
    shares = [m.participation_share for m in self._members if m.participation_share is not None]

    if not shares:
        return

    if len(shares) != len(self._members):
        raise ValueError("All members must have participation shares if any do")

    total = sum(shares)
    if abs(total - 1.0) > 0.0001:
        raise ValueError(f"Participation shares must sum to 1.0, got {total:.4f}")

Party dataclass

Bases: Canonicalize

A party in a bilateral relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass
class Party(Canonicalize):
    """A party in a bilateral relationship."""

    entity_id: str
    role_uri: str

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the party.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields with entityId and roleUri.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        insert_required(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the party.

Returns:

dict[str, str] A dictionary containing the canonical fields with entityId and roleUri.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
20
21
22
23
24
25
26
27
28
29
30
31
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the party.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields with entityId and roleUri.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    insert_required(fields, "roleUri", self.role_uri)
    return fields

RelationshipRecord dataclass

Bases: Canonicalize

A complete CEP Relationship Record.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
@dataclass
class RelationshipRecord(Canonicalize):
    """A complete CEP Relationship Record."""

    # Required fields
    verifiable_id: str
    relationship_type_uri: str
    parties: Parties
    effective_timestamp: CanonicalTimestamp
    status: RelationshipStatus
    jurisdiction_iso: str
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    parent_relationship_id: str | None = None
    expiration_timestamp: CanonicalTimestamp | None = None
    financial_terms: FinancialTerms | None = None
    terms_attributes: dict[str, str] | None = None
    source_references: list[SourceReference] | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new_bilateral(
        cls,
        verifiable_id: str,
        relationship_type_uri: str,
        parties: BilateralParties,
        effective_timestamp: CanonicalTimestamp,
        status: RelationshipStatus,
        jurisdiction_iso: str,
        attestation: Attestation,
    ) -> "RelationshipRecord":
        """Create a new bilateral RelationshipRecord."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_type_uri=relationship_type_uri,
            parties=parties,
            effective_timestamp=effective_timestamp,
            status=status,
            jurisdiction_iso=jurisdiction_iso,
            attestation=attestation,
        )

    @classmethod
    def new_multilateral(
        cls,
        verifiable_id: str,
        relationship_type_uri: str,
        members: MultilateralMembers,
        effective_timestamp: CanonicalTimestamp,
        status: RelationshipStatus,
        jurisdiction_iso: str,
        attestation: Attestation,
    ) -> "RelationshipRecord":
        """Create a new multilateral RelationshipRecord."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_type_uri=relationship_type_uri,
            parties=members,
            effective_timestamp=effective_timestamp,
            status=status,
            jurisdiction_iso=jurisdiction_iso,
            attestation=attestation,
        )

    def with_parent(self, parent_id: str) -> "RelationshipRecord":
        """Return a new RelationshipRecord with parent relationship set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=parent_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_expiration(self, timestamp: CanonicalTimestamp) -> "RelationshipRecord":
        """Return a new RelationshipRecord with expiration timestamp set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_financial_terms(self, terms: FinancialTerms) -> "RelationshipRecord":
        """Return a new RelationshipRecord with financial terms set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_source_reference(self, reference: SourceReference) -> "RelationshipRecord":
        """Return a new RelationshipRecord with a source reference added."""
        refs = list(self.source_references) if self.source_references else []
        refs.append(reference)
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=refs,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "RelationshipRecord":
        """Return a new RelationshipRecord with previous hash set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "RelationshipRecord":
        """Return a new RelationshipRecord with revision number set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())
        insert_required(
            fields, "effectiveTimestamp", self.effective_timestamp.to_canonical_string()
        )
        if self.expiration_timestamp is not None:
            insert_required(
                fields, "expirationTimestamp", self.expiration_timestamp.to_canonical_string()
            )
        if self.financial_terms is not None:
            insert_required(fields, "financialTerms", self.financial_terms.to_canonical_string())
        insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
        insert_if_present(fields, "parentRelationshipId", self.parent_relationship_id)

        # Parties (bilateral or multilateral)
        if isinstance(self.parties, BilateralParties):
            insert_required(fields, "bilateralParties", self.parties.to_canonical_string())
        else:
            insert_required(fields, "multilateralMembers", self.parties.to_canonical_string())

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())
        insert_required(fields, "relationshipTypeUri", self.relationship_type_uri)
        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Source references sorted by sourceSystemUri then sourceRecordId
        if self.source_references:
            sorted_refs = sorted(
                self.source_references,
                key=lambda r: (r.source_system_uri, r.source_record_id),
            )
            refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
            fields["sourceReferences"] = f"[{refs_json}]"

        insert_required(fields, "status", self.status.to_canonical_string())

        # Terms attributes (already sorted as dict)
        if self.terms_attributes:
            import json

            fields["termsAttributes"] = json.dumps(
                dict(sorted(self.terms_attributes.items())), separators=(",", ":")
            )

        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())
    insert_required(
        fields, "effectiveTimestamp", self.effective_timestamp.to_canonical_string()
    )
    if self.expiration_timestamp is not None:
        insert_required(
            fields, "expirationTimestamp", self.expiration_timestamp.to_canonical_string()
        )
    if self.financial_terms is not None:
        insert_required(fields, "financialTerms", self.financial_terms.to_canonical_string())
    insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
    insert_if_present(fields, "parentRelationshipId", self.parent_relationship_id)

    # Parties (bilateral or multilateral)
    if isinstance(self.parties, BilateralParties):
        insert_required(fields, "bilateralParties", self.parties.to_canonical_string())
    else:
        insert_required(fields, "multilateralMembers", self.parties.to_canonical_string())

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())
    insert_required(fields, "relationshipTypeUri", self.relationship_type_uri)
    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Source references sorted by sourceSystemUri then sourceRecordId
    if self.source_references:
        sorted_refs = sorted(
            self.source_references,
            key=lambda r: (r.source_system_uri, r.source_record_id),
        )
        refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
        fields["sourceReferences"] = f"[{refs_json}]"

    insert_required(fields, "status", self.status.to_canonical_string())

    # Terms attributes (already sorted as dict)
    if self.terms_attributes:
        import json

        fields["termsAttributes"] = json.dumps(
            dict(sorted(self.terms_attributes.items())), separators=(",", ":")
        )

    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new_bilateral classmethod
new_bilateral(
    verifiable_id: str,
    relationship_type_uri: str,
    parties: BilateralParties,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> RelationshipRecord

Create a new bilateral RelationshipRecord.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@classmethod
def new_bilateral(
    cls,
    verifiable_id: str,
    relationship_type_uri: str,
    parties: BilateralParties,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> "RelationshipRecord":
    """Create a new bilateral RelationshipRecord."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_type_uri=relationship_type_uri,
        parties=parties,
        effective_timestamp=effective_timestamp,
        status=status,
        jurisdiction_iso=jurisdiction_iso,
        attestation=attestation,
    )
new_multilateral classmethod
new_multilateral(
    verifiable_id: str,
    relationship_type_uri: str,
    members: MultilateralMembers,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> RelationshipRecord

Create a new multilateral RelationshipRecord.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
@classmethod
def new_multilateral(
    cls,
    verifiable_id: str,
    relationship_type_uri: str,
    members: MultilateralMembers,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> "RelationshipRecord":
    """Create a new multilateral RelationshipRecord."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_type_uri=relationship_type_uri,
        parties=members,
        effective_timestamp=effective_timestamp,
        status=status,
        jurisdiction_iso=jurisdiction_iso,
        attestation=attestation,
    )
with_expiration
with_expiration(
    timestamp: CanonicalTimestamp,
) -> RelationshipRecord

Return a new RelationshipRecord with expiration timestamp set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def with_expiration(self, timestamp: CanonicalTimestamp) -> "RelationshipRecord":
    """Return a new RelationshipRecord with expiration timestamp set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_financial_terms
with_financial_terms(
    terms: FinancialTerms,
) -> RelationshipRecord

Return a new RelationshipRecord with financial terms set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def with_financial_terms(self, terms: FinancialTerms) -> "RelationshipRecord":
    """Return a new RelationshipRecord with financial terms set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_parent
with_parent(parent_id: str) -> RelationshipRecord

Return a new RelationshipRecord with parent relationship set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def with_parent(self, parent_id: str) -> "RelationshipRecord":
    """Return a new RelationshipRecord with parent relationship set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=parent_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(
    hash_val: CanonicalHash,
) -> RelationshipRecord

Return a new RelationshipRecord with previous hash set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def with_previous_hash(self, hash_val: CanonicalHash) -> "RelationshipRecord":
    """Return a new RelationshipRecord with previous hash set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> RelationshipRecord

Return a new RelationshipRecord with revision number set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def with_revision(self, revision: int) -> "RelationshipRecord":
    """Return a new RelationshipRecord with revision number set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )
with_source_reference
with_source_reference(
    reference: SourceReference,
) -> RelationshipRecord

Return a new RelationshipRecord with a source reference added.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def with_source_reference(self, reference: SourceReference) -> "RelationshipRecord":
    """Return a new RelationshipRecord with a source reference added."""
    refs = list(self.source_references) if self.source_references else []
    refs.append(reference)
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=refs,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )

RelationshipStatus dataclass

Bases: Canonicalize

Relationship status information.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@dataclass
class RelationshipStatus(Canonicalize):
    """Relationship status information."""

    status_code: RelationshipStatusCode
    status_effective_timestamp: CanonicalTimestamp

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of relationship status fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(
            fields,
            "statusEffectiveTimestamp",
            self.status_effective_timestamp.to_canonical_string(),
        )
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of relationship status fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of relationship status fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(
        fields,
        "statusEffectiveTimestamp",
        self.status_effective_timestamp.to_canonical_string(),
    )
    return fields

RelationshipStatusCode

Bases: Enum

Relationship operational status.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
29
30
31
32
33
34
35
36
37
38
39
40
41
class RelationshipStatusCode(Enum):
    """Relationship operational status."""

    PENDING = "PENDING"
    ACTIVE = "ACTIVE"
    SUSPENDED = "SUSPENDED"
    COMPLETED = "COMPLETED"
    TERMINATED = "TERMINATED"
    AMENDED = "AMENDED"

    def as_str(self) -> str:
        """Return the string value of the relationship status code."""
        return self.value
as_str
as_str() -> str

Return the string value of the relationship status code.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
39
40
41
def as_str(self) -> str:
    """Return the string value of the relationship status code."""
    return self.value

SourceReference dataclass

Bases: Canonicalize

Reference to an authoritative source record.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@dataclass
class SourceReference(Canonicalize):
    """Reference to an authoritative source record."""

    source_system_uri: str
    source_record_id: str
    source_url: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of source reference fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "sourceRecordId", self.source_record_id)
        insert_required(fields, "sourceSystemUri", self.source_system_uri)
        insert_if_present(fields, "sourceUrl", self.source_url)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of source reference fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
102
103
104
105
106
107
108
109
110
111
112
113
114
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of source reference fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "sourceRecordId", self.source_record_id)
    insert_required(fields, "sourceSystemUri", self.source_system_uri)
    insert_if_present(fields, "sourceUrl", self.source_url)
    return fields

bilateral

Bilateral party definitions for two-party relationships.

Bilateral relationships have clear directionality: - Party A: The initiating, granting, or contracting party - Party B: The receiving, performing, or beneficiary party

BilateralParties dataclass

Bases: Canonicalize

Bilateral parties in a two-party relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass
class BilateralParties(Canonicalize):
    """Bilateral parties in a two-party relationship."""

    party_a: Party  # Initiating, granting, or contracting party
    party_b: Party  # Receiving, performing, or beneficiary party

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of bilateral parties.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields with partyA and partyB.
        """
        fields: dict[str, str] = {}
        # Nested objects serialized as their canonical strings
        insert_required(fields, "partyA", self.party_a.to_canonical_string())
        insert_required(fields, "partyB", self.party_b.to_canonical_string())
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of bilateral parties.

Returns:

dict[str, str] A dictionary containing the canonical fields with partyA and partyB.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
41
42
43
44
45
46
47
48
49
50
51
52
53
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of bilateral parties.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields with partyA and partyB.
    """
    fields: dict[str, str] = {}
    # Nested objects serialized as their canonical strings
    insert_required(fields, "partyA", self.party_a.to_canonical_string())
    insert_required(fields, "partyB", self.party_b.to_canonical_string())
    return fields
Party dataclass

Bases: Canonicalize

A party in a bilateral relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@dataclass
class Party(Canonicalize):
    """A party in a bilateral relationship."""

    entity_id: str
    role_uri: str

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the party.

        Returns:
        -------
        dict[str, str]
            A dictionary containing the canonical fields with entityId and roleUri.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        insert_required(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the party.

Returns:

dict[str, str] A dictionary containing the canonical fields with entityId and roleUri.

Source code in src/python/src/civic_exchange_protocol/relationship/bilateral.py
20
21
22
23
24
25
26
27
28
29
30
31
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the party.

    Returns:
    -------
    dict[str, str]
        A dictionary containing the canonical fields with entityId and roleUri.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    insert_required(fields, "roleUri", self.role_uri)
    return fields

multilateral

Multilateral member definitions for n-ary relationships.

Multilateral relationships involve more than two parties, such as: - Consortia - Joint ventures - Board memberships

Members are sorted by entity_id to guarantee deterministic ordering for hash stability across all implementations.

Member dataclass

Bases: Canonicalize

A member in a multilateral relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass
class Member(Canonicalize):
    """A member in a multilateral relationship."""

    entity_id: str
    role_uri: str
    participation_share: float | None = None

    def with_share(self, share: float) -> "Member":
        """Return a new Member with the participation share set."""
        return Member(
            entity_id=self.entity_id,
            role_uri=self.role_uri,
            participation_share=share,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical field representation of the member.

        Returns:
        -------
        dict[str, str]
            Dictionary containing entityId, roleUri, and optionally participationShare.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "entityId", self.entity_id)
        if self.participation_share is not None:
            insert_required(fields, "participationShare", f"{self.participation_share:.4f}")
        insert_required(fields, "roleUri", self.role_uri)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical field representation of the member.

Returns:

dict[str, str] Dictionary containing entityId, roleUri, and optionally participationShare.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical field representation of the member.

    Returns:
    -------
    dict[str, str]
        Dictionary containing entityId, roleUri, and optionally participationShare.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "entityId", self.entity_id)
    if self.participation_share is not None:
        insert_required(fields, "participationShare", f"{self.participation_share:.4f}")
    insert_required(fields, "roleUri", self.role_uri)
    return fields
with_share
with_share(share: float) -> Member

Return a new Member with the participation share set.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
26
27
28
29
30
31
32
def with_share(self, share: float) -> "Member":
    """Return a new Member with the participation share set."""
    return Member(
        entity_id=self.entity_id,
        role_uri=self.role_uri,
        participation_share=share,
    )
MultilateralMembers

Bases: Canonicalize

A collection of members in a multilateral relationship.

Members are automatically sorted by entity_id to ensure hash stability regardless of insertion order.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
class MultilateralMembers(Canonicalize):
    """A collection of members in a multilateral relationship.

    Members are automatically sorted by entity_id to ensure
    hash stability regardless of insertion order.
    """

    def __init__(self) -> None:
        """Initialize an empty collection of multilateral relationship members."""
        self._members: list[Member] = []

    def add(self, member: Member) -> None:
        """Add a member to the set."""
        # Check for duplicate entity_id
        for existing in self._members:
            if existing.entity_id == member.entity_id:
                return  # Already exists
        self._members.append(member)

    def __len__(self) -> int:
        """Return the number of members in the collection."""
        return len(self._members)

    def __iter__(self) -> Iterator[Member]:
        """Iterate over members in sorted order by entity_id."""
        return iter(sorted(self._members, key=lambda m: m.entity_id))

    def is_empty(self) -> bool:
        """Check if the collection has no members.

        Returns:
        -------
        bool
            True if the collection is empty, False otherwise.
        """
        return len(self._members) == 0

    def validate_shares(self) -> None:
        """Validate that all participation shares sum to 1.0 (if present).

        Raises:
            ValueError: If validation fails.
        """
        shares = [m.participation_share for m in self._members if m.participation_share is not None]

        if not shares:
            return

        if len(shares) != len(self._members):
            raise ValueError("All members must have participation shares if any do")

        total = sum(shares)
        if abs(total - 1.0) > 0.0001:
            raise ValueError(f"Participation shares must sum to 1.0, got {total:.4f}")

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields."""
        fields: dict[str, str] = {}

        # Serialize as array, members sorted by entity_id
        if self._members:
            sorted_members = sorted(self._members, key=lambda m: m.entity_id)
            members_json = ",".join(m.to_canonical_string() for m in sorted_members)
            fields["members"] = f"[{members_json}]"

        return fields
__init__
__init__() -> None

Initialize an empty collection of multilateral relationship members.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
57
58
59
def __init__(self) -> None:
    """Initialize an empty collection of multilateral relationship members."""
    self._members: list[Member] = []
__iter__
__iter__() -> Iterator[Member]

Iterate over members in sorted order by entity_id.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
73
74
75
def __iter__(self) -> Iterator[Member]:
    """Iterate over members in sorted order by entity_id."""
    return iter(sorted(self._members, key=lambda m: m.entity_id))
__len__
__len__() -> int

Return the number of members in the collection.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
69
70
71
def __len__(self) -> int:
    """Return the number of members in the collection."""
    return len(self._members)
add
add(member: Member) -> None

Add a member to the set.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
61
62
63
64
65
66
67
def add(self, member: Member) -> None:
    """Add a member to the set."""
    # Check for duplicate entity_id
    for existing in self._members:
        if existing.entity_id == member.entity_id:
            return  # Already exists
    self._members.append(member)
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
105
106
107
108
109
110
111
112
113
114
115
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields."""
    fields: dict[str, str] = {}

    # Serialize as array, members sorted by entity_id
    if self._members:
        sorted_members = sorted(self._members, key=lambda m: m.entity_id)
        members_json = ",".join(m.to_canonical_string() for m in sorted_members)
        fields["members"] = f"[{members_json}]"

    return fields
is_empty
is_empty() -> bool

Check if the collection has no members.

Returns:

bool True if the collection is empty, False otherwise.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
77
78
79
80
81
82
83
84
85
def is_empty(self) -> bool:
    """Check if the collection has no members.

    Returns:
    -------
    bool
        True if the collection is empty, False otherwise.
    """
    return len(self._members) == 0
validate_shares
validate_shares() -> None

Validate that all participation shares sum to 1.0 (if present).

Raises:

Type Description
ValueError

If validation fails.

Source code in src/python/src/civic_exchange_protocol/relationship/multilateral.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
def validate_shares(self) -> None:
    """Validate that all participation shares sum to 1.0 (if present).

    Raises:
        ValueError: If validation fails.
    """
    shares = [m.participation_share for m in self._members if m.participation_share is not None]

    if not shares:
        return

    if len(shares) != len(self._members):
        raise ValueError("All members must have participation shares if any do")

    total = sum(shares)
    if abs(total - 1.0) > 0.0001:
        raise ValueError(f"Participation shares must sum to 1.0, got {total:.4f}")

relationship

CEP Relationship Record definition.

A Relationship Record represents a verifiable legal or functional relationship between two or more attested entities.

Relationships can be: - Bilateral: Two-party relationships with clear directionality (contracts, grants) - Multilateral: N-ary relationships (consortia, boards, joint ventures)

FinancialTerms dataclass

Bases: Canonicalize

Financial terms of a relationship.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@dataclass
class FinancialTerms(Canonicalize):
    """Financial terms of a relationship."""

    total_value: float | None = None
    obligated_value: float | None = None
    currency_code: str = "USD"

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of financial terms fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "currencyCode", self.currency_code)
        if self.obligated_value is not None:
            insert_required(fields, "obligatedValue", format_amount(self.obligated_value))
        if self.total_value is not None:
            insert_required(fields, "totalValue", format_amount(self.total_value))
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of financial terms fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of financial terms fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "currencyCode", self.currency_code)
    if self.obligated_value is not None:
        insert_required(fields, "obligatedValue", format_amount(self.obligated_value))
    if self.total_value is not None:
        insert_required(fields, "totalValue", format_amount(self.total_value))
    return fields
RelationshipRecord dataclass

Bases: Canonicalize

A complete CEP Relationship Record.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
@dataclass
class RelationshipRecord(Canonicalize):
    """A complete CEP Relationship Record."""

    # Required fields
    verifiable_id: str
    relationship_type_uri: str
    parties: Parties
    effective_timestamp: CanonicalTimestamp
    status: RelationshipStatus
    jurisdiction_iso: str
    attestation: Attestation

    # Optional fields
    schema_version: str = field(default=SCHEMA_VERSION)
    parent_relationship_id: str | None = None
    expiration_timestamp: CanonicalTimestamp | None = None
    financial_terms: FinancialTerms | None = None
    terms_attributes: dict[str, str] | None = None
    source_references: list[SourceReference] | None = None
    previous_record_hash: CanonicalHash | None = None
    revision_number: int = 1

    @classmethod
    def new_bilateral(
        cls,
        verifiable_id: str,
        relationship_type_uri: str,
        parties: BilateralParties,
        effective_timestamp: CanonicalTimestamp,
        status: RelationshipStatus,
        jurisdiction_iso: str,
        attestation: Attestation,
    ) -> "RelationshipRecord":
        """Create a new bilateral RelationshipRecord."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_type_uri=relationship_type_uri,
            parties=parties,
            effective_timestamp=effective_timestamp,
            status=status,
            jurisdiction_iso=jurisdiction_iso,
            attestation=attestation,
        )

    @classmethod
    def new_multilateral(
        cls,
        verifiable_id: str,
        relationship_type_uri: str,
        members: MultilateralMembers,
        effective_timestamp: CanonicalTimestamp,
        status: RelationshipStatus,
        jurisdiction_iso: str,
        attestation: Attestation,
    ) -> "RelationshipRecord":
        """Create a new multilateral RelationshipRecord."""
        return cls(
            verifiable_id=verifiable_id,
            relationship_type_uri=relationship_type_uri,
            parties=members,
            effective_timestamp=effective_timestamp,
            status=status,
            jurisdiction_iso=jurisdiction_iso,
            attestation=attestation,
        )

    def with_parent(self, parent_id: str) -> "RelationshipRecord":
        """Return a new RelationshipRecord with parent relationship set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=parent_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_expiration(self, timestamp: CanonicalTimestamp) -> "RelationshipRecord":
        """Return a new RelationshipRecord with expiration timestamp set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_financial_terms(self, terms: FinancialTerms) -> "RelationshipRecord":
        """Return a new RelationshipRecord with financial terms set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_source_reference(self, reference: SourceReference) -> "RelationshipRecord":
        """Return a new RelationshipRecord with a source reference added."""
        refs = list(self.source_references) if self.source_references else []
        refs.append(reference)
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=refs,
            previous_record_hash=self.previous_record_hash,
            revision_number=self.revision_number,
        )

    def with_previous_hash(self, hash_val: CanonicalHash) -> "RelationshipRecord":
        """Return a new RelationshipRecord with previous hash set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=hash_val,
            revision_number=self.revision_number,
        )

    def with_revision(self, revision: int) -> "RelationshipRecord":
        """Return a new RelationshipRecord with revision number set."""
        return RelationshipRecord(
            verifiable_id=self.verifiable_id,
            relationship_type_uri=self.relationship_type_uri,
            parties=self.parties,
            effective_timestamp=self.effective_timestamp,
            status=self.status,
            jurisdiction_iso=self.jurisdiction_iso,
            attestation=self.attestation,
            schema_version=self.schema_version,
            parent_relationship_id=self.parent_relationship_id,
            expiration_timestamp=self.expiration_timestamp,
            financial_terms=self.financial_terms,
            terms_attributes=self.terms_attributes,
            source_references=self.source_references,
            previous_record_hash=self.previous_record_hash,
            revision_number=revision,
        )

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical fields in alphabetical order."""
        fields: dict[str, str] = {}

        # All fields in alphabetical order
        insert_required(fields, "attestation", self.attestation.to_canonical_string())
        insert_required(
            fields, "effectiveTimestamp", self.effective_timestamp.to_canonical_string()
        )
        if self.expiration_timestamp is not None:
            insert_required(
                fields, "expirationTimestamp", self.expiration_timestamp.to_canonical_string()
            )
        if self.financial_terms is not None:
            insert_required(fields, "financialTerms", self.financial_terms.to_canonical_string())
        insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
        insert_if_present(fields, "parentRelationshipId", self.parent_relationship_id)

        # Parties (bilateral or multilateral)
        if isinstance(self.parties, BilateralParties):
            insert_required(fields, "bilateralParties", self.parties.to_canonical_string())
        else:
            insert_required(fields, "multilateralMembers", self.parties.to_canonical_string())

        if self.previous_record_hash is not None:
            insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())
        insert_required(fields, "relationshipTypeUri", self.relationship_type_uri)
        insert_required(fields, "revisionNumber", str(self.revision_number))
        insert_required(fields, "schemaVersion", self.schema_version)

        # Source references sorted by sourceSystemUri then sourceRecordId
        if self.source_references:
            sorted_refs = sorted(
                self.source_references,
                key=lambda r: (r.source_system_uri, r.source_record_id),
            )
            refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
            fields["sourceReferences"] = f"[{refs_json}]"

        insert_required(fields, "status", self.status.to_canonical_string())

        # Terms attributes (already sorted as dict)
        if self.terms_attributes:
            import json

            fields["termsAttributes"] = json.dumps(
                dict(sorted(self.terms_attributes.items())), separators=(",", ":")
            )

        insert_required(fields, "verifiableId", self.verifiable_id)

        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical fields in alphabetical order.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical fields in alphabetical order."""
    fields: dict[str, str] = {}

    # All fields in alphabetical order
    insert_required(fields, "attestation", self.attestation.to_canonical_string())
    insert_required(
        fields, "effectiveTimestamp", self.effective_timestamp.to_canonical_string()
    )
    if self.expiration_timestamp is not None:
        insert_required(
            fields, "expirationTimestamp", self.expiration_timestamp.to_canonical_string()
        )
    if self.financial_terms is not None:
        insert_required(fields, "financialTerms", self.financial_terms.to_canonical_string())
    insert_required(fields, "jurisdictionIso", self.jurisdiction_iso)
    insert_if_present(fields, "parentRelationshipId", self.parent_relationship_id)

    # Parties (bilateral or multilateral)
    if isinstance(self.parties, BilateralParties):
        insert_required(fields, "bilateralParties", self.parties.to_canonical_string())
    else:
        insert_required(fields, "multilateralMembers", self.parties.to_canonical_string())

    if self.previous_record_hash is not None:
        insert_required(fields, "previousRecordHash", self.previous_record_hash.as_hex())
    insert_required(fields, "relationshipTypeUri", self.relationship_type_uri)
    insert_required(fields, "revisionNumber", str(self.revision_number))
    insert_required(fields, "schemaVersion", self.schema_version)

    # Source references sorted by sourceSystemUri then sourceRecordId
    if self.source_references:
        sorted_refs = sorted(
            self.source_references,
            key=lambda r: (r.source_system_uri, r.source_record_id),
        )
        refs_json = ",".join(r.to_canonical_string() for r in sorted_refs)
        fields["sourceReferences"] = f"[{refs_json}]"

    insert_required(fields, "status", self.status.to_canonical_string())

    # Terms attributes (already sorted as dict)
    if self.terms_attributes:
        import json

        fields["termsAttributes"] = json.dumps(
            dict(sorted(self.terms_attributes.items())), separators=(",", ":")
        )

    insert_required(fields, "verifiableId", self.verifiable_id)

    return fields
new_bilateral classmethod
new_bilateral(
    verifiable_id: str,
    relationship_type_uri: str,
    parties: BilateralParties,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> RelationshipRecord

Create a new bilateral RelationshipRecord.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@classmethod
def new_bilateral(
    cls,
    verifiable_id: str,
    relationship_type_uri: str,
    parties: BilateralParties,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> "RelationshipRecord":
    """Create a new bilateral RelationshipRecord."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_type_uri=relationship_type_uri,
        parties=parties,
        effective_timestamp=effective_timestamp,
        status=status,
        jurisdiction_iso=jurisdiction_iso,
        attestation=attestation,
    )
new_multilateral classmethod
new_multilateral(
    verifiable_id: str,
    relationship_type_uri: str,
    members: MultilateralMembers,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> RelationshipRecord

Create a new multilateral RelationshipRecord.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
@classmethod
def new_multilateral(
    cls,
    verifiable_id: str,
    relationship_type_uri: str,
    members: MultilateralMembers,
    effective_timestamp: CanonicalTimestamp,
    status: RelationshipStatus,
    jurisdiction_iso: str,
    attestation: Attestation,
) -> "RelationshipRecord":
    """Create a new multilateral RelationshipRecord."""
    return cls(
        verifiable_id=verifiable_id,
        relationship_type_uri=relationship_type_uri,
        parties=members,
        effective_timestamp=effective_timestamp,
        status=status,
        jurisdiction_iso=jurisdiction_iso,
        attestation=attestation,
    )
with_expiration
with_expiration(
    timestamp: CanonicalTimestamp,
) -> RelationshipRecord

Return a new RelationshipRecord with expiration timestamp set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
def with_expiration(self, timestamp: CanonicalTimestamp) -> "RelationshipRecord":
    """Return a new RelationshipRecord with expiration timestamp set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_financial_terms
with_financial_terms(
    terms: FinancialTerms,
) -> RelationshipRecord

Return a new RelationshipRecord with financial terms set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def with_financial_terms(self, terms: FinancialTerms) -> "RelationshipRecord":
    """Return a new RelationshipRecord with financial terms set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_parent
with_parent(parent_id: str) -> RelationshipRecord

Return a new RelationshipRecord with parent relationship set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def with_parent(self, parent_id: str) -> "RelationshipRecord":
    """Return a new RelationshipRecord with parent relationship set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=parent_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
with_previous_hash
with_previous_hash(
    hash_val: CanonicalHash,
) -> RelationshipRecord

Return a new RelationshipRecord with previous hash set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
def with_previous_hash(self, hash_val: CanonicalHash) -> "RelationshipRecord":
    """Return a new RelationshipRecord with previous hash set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=hash_val,
        revision_number=self.revision_number,
    )
with_revision
with_revision(revision: int) -> RelationshipRecord

Return a new RelationshipRecord with revision number set.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def with_revision(self, revision: int) -> "RelationshipRecord":
    """Return a new RelationshipRecord with revision number set."""
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=self.source_references,
        previous_record_hash=self.previous_record_hash,
        revision_number=revision,
    )
with_source_reference
with_source_reference(
    reference: SourceReference,
) -> RelationshipRecord

Return a new RelationshipRecord with a source reference added.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
def with_source_reference(self, reference: SourceReference) -> "RelationshipRecord":
    """Return a new RelationshipRecord with a source reference added."""
    refs = list(self.source_references) if self.source_references else []
    refs.append(reference)
    return RelationshipRecord(
        verifiable_id=self.verifiable_id,
        relationship_type_uri=self.relationship_type_uri,
        parties=self.parties,
        effective_timestamp=self.effective_timestamp,
        status=self.status,
        jurisdiction_iso=self.jurisdiction_iso,
        attestation=self.attestation,
        schema_version=self.schema_version,
        parent_relationship_id=self.parent_relationship_id,
        expiration_timestamp=self.expiration_timestamp,
        financial_terms=self.financial_terms,
        terms_attributes=self.terms_attributes,
        source_references=refs,
        previous_record_hash=self.previous_record_hash,
        revision_number=self.revision_number,
    )
RelationshipStatus dataclass

Bases: Canonicalize

Relationship status information.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@dataclass
class RelationshipStatus(Canonicalize):
    """Relationship status information."""

    status_code: RelationshipStatusCode
    status_effective_timestamp: CanonicalTimestamp

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of relationship status fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "statusCode", self.status_code.as_str())
        insert_required(
            fields,
            "statusEffectiveTimestamp",
            self.status_effective_timestamp.to_canonical_string(),
        )
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of relationship status fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of relationship status fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "statusCode", self.status_code.as_str())
    insert_required(
        fields,
        "statusEffectiveTimestamp",
        self.status_effective_timestamp.to_canonical_string(),
    )
    return fields
RelationshipStatusCode

Bases: Enum

Relationship operational status.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
29
30
31
32
33
34
35
36
37
38
39
40
41
class RelationshipStatusCode(Enum):
    """Relationship operational status."""

    PENDING = "PENDING"
    ACTIVE = "ACTIVE"
    SUSPENDED = "SUSPENDED"
    COMPLETED = "COMPLETED"
    TERMINATED = "TERMINATED"
    AMENDED = "AMENDED"

    def as_str(self) -> str:
        """Return the string value of the relationship status code."""
        return self.value
as_str
as_str() -> str

Return the string value of the relationship status code.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
39
40
41
def as_str(self) -> str:
    """Return the string value of the relationship status code."""
    return self.value
SourceReference dataclass

Bases: Canonicalize

Reference to an authoritative source record.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
@dataclass
class SourceReference(Canonicalize):
    """Reference to an authoritative source record."""

    source_system_uri: str
    source_record_id: str
    source_url: str | None = None

    def canonical_fields(self) -> dict[str, str]:
        """Return the canonical representation of source reference fields.

        Returns:
        -------
        dict[str, str]
            Dictionary containing the canonical field representations.
        """
        fields: dict[str, str] = {}
        insert_required(fields, "sourceRecordId", self.source_record_id)
        insert_required(fields, "sourceSystemUri", self.source_system_uri)
        insert_if_present(fields, "sourceUrl", self.source_url)
        return fields
canonical_fields
canonical_fields() -> dict[str, str]

Return the canonical representation of source reference fields.

Returns:

dict[str, str] Dictionary containing the canonical field representations.

Source code in src/python/src/civic_exchange_protocol/relationship/relationship.py
102
103
104
105
106
107
108
109
110
111
112
113
114
def canonical_fields(self) -> dict[str, str]:
    """Return the canonical representation of source reference fields.

    Returns:
    -------
    dict[str, str]
        Dictionary containing the canonical field representations.
    """
    fields: dict[str, str] = {}
    insert_required(fields, "sourceRecordId", self.source_record_id)
    insert_required(fields, "sourceSystemUri", self.source_system_uri)
    insert_if_present(fields, "sourceUrl", self.source_url)
    return fields

snfei

CEP Core Linker: Entity Resolution and SNFEI Generation.

This package implements the Normalizing Functor architecture for generating deterministic entity identifiers (SNFEIs) from heterogeneous source data.

Architecture

┌──────────────┐ ┌────────────────┐ ┌─────────────┐ │ Raw Entity │ │ Intermediate │ │ Canonical │ │ Data │───> │ Canonical │───> │ Entity │ │ │ L │ │ N │ │ └──────────────┘ └────────────────┘ └─────────────┘ │ │ SHA-256 V ┌──────────────┐ │ SNFEI │ │ (64-char) │ └──────────────┘

L = Localization Functor (jurisdiction-specific transforms) N = Normalizing Functor (universal normalization)

Usage

from civic_exchange_protocol.core_linker import ( generate_snfei, normalize_legal_name, apply_localization, )

Simple SNFEI generation

snfei, inputs = generate_snfei( legal_name="Springfield USD #12", country_code="US", address="123 Main St", )

With jurisdiction-specific localization

from civic_exchange_protocol.core_linker import apply_localization localized = apply_localization("MTA", "US/NY")

-> "metropolitan transportation authority"

CanonicalInput dataclass

Normalized input for SNFEI hashing.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
@dataclass
class CanonicalInput:
    """Normalized input for SNFEI hashing."""

    legal_name_normalized: str
    address_normalized: str | None
    country_code: str
    registration_date: str | None

    def to_hash_string(self) -> str:
        """Generate the concatenated string for hashing.

        Format:
            legal_name_normalized|address_normalized|country_code|registration_date

        Empty/None fields are included as empty strings to maintain
        consistent field positions.
        """
        parts = [
            self.legal_name_normalized,
            self.address_normalized or "",
            self.country_code,
            self.registration_date or "",
        ]
        return "|".join(parts)

    def to_hash_string_v2(self) -> str:
        """Alternative format that omits empty fields.

        This produces shorter strings but requires all implementations
        to handle optional fields identically.
        """
        parts = [self.legal_name_normalized]
        if self.address_normalized:
            parts.append(self.address_normalized)
        parts.append(self.country_code)
        if self.registration_date:
            parts.append(self.registration_date)
        return "|".join(parts)
to_hash_string
to_hash_string() -> str

Generate the concatenated string for hashing.

Format

legal_name_normalized|address_normalized|country_code|registration_date

Empty/None fields are included as empty strings to maintain consistent field positions.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def to_hash_string(self) -> str:
    """Generate the concatenated string for hashing.

    Format:
        legal_name_normalized|address_normalized|country_code|registration_date

    Empty/None fields are included as empty strings to maintain
    consistent field positions.
    """
    parts = [
        self.legal_name_normalized,
        self.address_normalized or "",
        self.country_code,
        self.registration_date or "",
    ]
    return "|".join(parts)
to_hash_string_v2
to_hash_string_v2() -> str

Alternative format that omits empty fields.

This produces shorter strings but requires all implementations to handle optional fields identically.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
580
581
582
583
584
585
586
587
588
589
590
591
592
def to_hash_string_v2(self) -> str:
    """Alternative format that omits empty fields.

    This produces shorter strings but requires all implementations
    to handle optional fields identically.
    """
    parts = [self.legal_name_normalized]
    if self.address_normalized:
        parts.append(self.address_normalized)
    parts.append(self.country_code)
    if self.registration_date:
        parts.append(self.registration_date)
    return "|".join(parts)

LocalizationConfig dataclass

Configuration for a specific jurisdiction.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@dataclass
class LocalizationConfig:
    """Configuration for a specific jurisdiction."""

    jurisdiction: str  # e.g., "US/CA", "CA/ON"
    parent: str | None  # Parent jurisdiction for inheritance

    # Transformation maps
    abbreviations: dict[str, str] = field(default_factory=dict)
    agency_names: dict[str, str] = field(default_factory=dict)
    entity_types: dict[str, str] = field(default_factory=dict)

    # Additional rules
    rules: list[LocalizationRule] = field(default_factory=list)

    # Stop words specific to this jurisdiction
    stop_words: set[str] = field(default_factory=set)

    def apply_to_name(self, name: str) -> str:
        """Apply jurisdiction-specific transformations to a name.

        Order of application:
        1. Agency name expansions
        2. Abbreviation expansions
        3. Entity type standardization
        4. Custom rules
        """
        result = name.lower()

        # 1. Agency names (exact match, case-insensitive)
        for abbrev, full in self.agency_names.items():
            # Word boundary matching
            import re

            pattern = r"\b" + re.escape(abbrev.lower()) + r"\b"
            result = re.sub(pattern, full.lower(), result)

        # 2. Abbreviations
        tokens = result.split()
        expanded = []
        for token in tokens:
            if token in self.abbreviations:
                expanded.append(self.abbreviations[token].lower())
            else:
                expanded.append(token)
        result = " ".join(expanded)

        # 3. Entity types
        for local_type, canonical_type in self.entity_types.items():
            import re

            pattern = r"\b" + re.escape(local_type.lower()) + r"\b"
            result = re.sub(pattern, canonical_type.lower(), result)

        # 4. Custom rules
        for rule in self.rules:
            if rule.is_regex:
                import re

                result = re.sub(rule.pattern, rule.replacement, result, flags=re.IGNORECASE)
            else:
                result = result.replace(rule.pattern.lower(), rule.replacement.lower())

        return result
apply_to_name
apply_to_name(name: str) -> str

Apply jurisdiction-specific transformations to a name.

Order of application: 1. Agency name expansions 2. Abbreviation expansions 3. Entity type standardization 4. Custom rules

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def apply_to_name(self, name: str) -> str:
    """Apply jurisdiction-specific transformations to a name.

    Order of application:
    1. Agency name expansions
    2. Abbreviation expansions
    3. Entity type standardization
    4. Custom rules
    """
    result = name.lower()

    # 1. Agency names (exact match, case-insensitive)
    for abbrev, full in self.agency_names.items():
        # Word boundary matching
        import re

        pattern = r"\b" + re.escape(abbrev.lower()) + r"\b"
        result = re.sub(pattern, full.lower(), result)

    # 2. Abbreviations
    tokens = result.split()
    expanded = []
    for token in tokens:
        if token in self.abbreviations:
            expanded.append(self.abbreviations[token].lower())
        else:
            expanded.append(token)
    result = " ".join(expanded)

    # 3. Entity types
    for local_type, canonical_type in self.entity_types.items():
        import re

        pattern = r"\b" + re.escape(local_type.lower()) + r"\b"
        result = re.sub(pattern, canonical_type.lower(), result)

    # 4. Custom rules
    for rule in self.rules:
        if rule.is_regex:
            import re

            result = re.sub(rule.pattern, rule.replacement, result, flags=re.IGNORECASE)
        else:
            result = result.replace(rule.pattern.lower(), rule.replacement.lower())

    return result

LocalizationRegistry

Registry for loading and caching localization configurations.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
class LocalizationRegistry:
    """Registry for loading and caching localization configurations."""

    def __init__(self, config_dir: Path | None = None):
        """Initialize the registry.

        Args:
            config_dir: Optional path to localization YAML files.
                       If None, only built-in configs are available.
        """
        self.config_dir = config_dir
        self._cache: dict[str, LocalizationConfig] = dict(BUILT_IN_CONFIGS)

    def get_config(self, jurisdiction: str) -> LocalizationConfig:
        """Get localization config for a jurisdiction.

        Falls back through parent jurisdictions if specific config not found.
        Merges child config with parent config for inheritance.

        Args:
            jurisdiction: Jurisdiction code (e.g., "us/ca", "ca/on").
                         Case-insensitive - will be normalized to lowercase.

        Returns:
            LocalizationConfig for the jurisdiction (merged with parent).
        """
        # Normalize to lowercase
        jurisdiction = jurisdiction.lower()

        # Check if we have a merged config cached
        cache_key = f"_merged_{jurisdiction}"
        if cache_key in self._cache:
            return self._cache[cache_key]

        # Get the base config for this jurisdiction
        if jurisdiction in self._cache:
            config = self._cache[jurisdiction]
        elif self.config_dir:
            config = self._load_yaml(jurisdiction)
            if config:
                self._cache[jurisdiction] = config
            else:
                config = None
        else:
            config = None

        # If no config found, fall back to parent
        if config is None:
            if "/" in jurisdiction:
                parent = jurisdiction.rsplit("/", 1)[0]
                return self.get_config(parent)
            # Return empty config as last resort
            return LocalizationConfig(jurisdiction=jurisdiction, parent=None)

        # If config has a parent, merge with parent config
        if config.parent:
            parent_config = self.get_config(config.parent)
            merged = self.merge_configs(config, parent_config)
            self._cache[cache_key] = merged
            return merged

        return config

    def _load_yaml(self, jurisdiction: str) -> LocalizationConfig | None:
        """Load config from YAML file.

        Expected paths:
            - {config_dir}/{country}/base.yaml for country-level (e.g., US, CA)
            - {config_dir}/{country}/{region}.yaml for region-level (e.g., US/CA, CA/ON)
        """
        if not self.config_dir:
            return None

        # Determine the YAML file path
        if "/" in jurisdiction:
            # Region-level: US/CA -> US/CA.yaml
            parts = jurisdiction.split("/")
            yaml_path = self.config_dir / parts[0] / f"{parts[1]}.yaml"
        else:
            # Country-level: US -> US/base.yaml
            yaml_path = self.config_dir / jurisdiction / "base.yaml"

        if not yaml_path.exists():
            return None

        try:
            with yaml_path.open("r", encoding="utf-8") as f:
                data = yaml.safe_load(f)

            if not data:
                return None

            # Parse rules if present
            rules = []
            for rule_data in data.get("rules", []):
                rules.append(
                    LocalizationRule(
                        pattern=rule_data.get("pattern", ""),
                        replacement=rule_data.get("replacement", ""),
                        is_regex=rule_data.get("is_regex", False),
                        context=rule_data.get("context"),
                    )
                )

            return LocalizationConfig(
                jurisdiction=data.get("jurisdiction", jurisdiction),
                parent=data.get("parent"),
                abbreviations=data.get("abbreviations", {}),
                agency_names=data.get("agency_names", {}),
                entity_types=data.get("entity_types", {}),
                rules=rules,
                stop_words=set(data.get("stop_words", [])),
            )
        except Exception as e:
            # Log error but don't crash
            print(f"Warning: Failed to load localization YAML {yaml_path}: {e}")
            return None

    def merge_configs(
        self, child: LocalizationConfig, parent: LocalizationConfig
    ) -> LocalizationConfig:
        """Merge child config with parent (child overrides parent)."""
        merged_abbrevs = dict(parent.abbreviations)
        merged_abbrevs.update(child.abbreviations)

        merged_agencies = dict(parent.agency_names)
        merged_agencies.update(child.agency_names)

        merged_types = dict(parent.entity_types)
        merged_types.update(child.entity_types)

        return LocalizationConfig(
            jurisdiction=child.jurisdiction,
            parent=parent.jurisdiction,
            abbreviations=merged_abbrevs,
            agency_names=merged_agencies,
            entity_types=merged_types,
            rules=parent.rules + child.rules,
            stop_words=parent.stop_words | child.stop_words,
        )
__init__
__init__(config_dir: Path | None = None)

Initialize the registry.

Parameters:

Name Type Description Default
config_dir Path | None

Optional path to localization YAML files. If None, only built-in configs are available.

None
Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
269
270
271
272
273
274
275
276
277
def __init__(self, config_dir: Path | None = None):
    """Initialize the registry.

    Args:
        config_dir: Optional path to localization YAML files.
                   If None, only built-in configs are available.
    """
    self.config_dir = config_dir
    self._cache: dict[str, LocalizationConfig] = dict(BUILT_IN_CONFIGS)
get_config
get_config(jurisdiction: str) -> LocalizationConfig

Get localization config for a jurisdiction.

Falls back through parent jurisdictions if specific config not found. Merges child config with parent config for inheritance.

Parameters:

Name Type Description Default
jurisdiction str

Jurisdiction code (e.g., "us/ca", "ca/on"). Case-insensitive - will be normalized to lowercase.

required

Returns:

Type Description
LocalizationConfig

LocalizationConfig for the jurisdiction (merged with parent).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def get_config(self, jurisdiction: str) -> LocalizationConfig:
    """Get localization config for a jurisdiction.

    Falls back through parent jurisdictions if specific config not found.
    Merges child config with parent config for inheritance.

    Args:
        jurisdiction: Jurisdiction code (e.g., "us/ca", "ca/on").
                     Case-insensitive - will be normalized to lowercase.

    Returns:
        LocalizationConfig for the jurisdiction (merged with parent).
    """
    # Normalize to lowercase
    jurisdiction = jurisdiction.lower()

    # Check if we have a merged config cached
    cache_key = f"_merged_{jurisdiction}"
    if cache_key in self._cache:
        return self._cache[cache_key]

    # Get the base config for this jurisdiction
    if jurisdiction in self._cache:
        config = self._cache[jurisdiction]
    elif self.config_dir:
        config = self._load_yaml(jurisdiction)
        if config:
            self._cache[jurisdiction] = config
        else:
            config = None
    else:
        config = None

    # If no config found, fall back to parent
    if config is None:
        if "/" in jurisdiction:
            parent = jurisdiction.rsplit("/", 1)[0]
            return self.get_config(parent)
        # Return empty config as last resort
        return LocalizationConfig(jurisdiction=jurisdiction, parent=None)

    # If config has a parent, merge with parent config
    if config.parent:
        parent_config = self.get_config(config.parent)
        merged = self.merge_configs(config, parent_config)
        self._cache[cache_key] = merged
        return merged

    return config
merge_configs
merge_configs(
    child: LocalizationConfig, parent: LocalizationConfig
) -> LocalizationConfig

Merge child config with parent (child overrides parent).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
def merge_configs(
    self, child: LocalizationConfig, parent: LocalizationConfig
) -> LocalizationConfig:
    """Merge child config with parent (child overrides parent)."""
    merged_abbrevs = dict(parent.abbreviations)
    merged_abbrevs.update(child.abbreviations)

    merged_agencies = dict(parent.agency_names)
    merged_agencies.update(child.agency_names)

    merged_types = dict(parent.entity_types)
    merged_types.update(child.entity_types)

    return LocalizationConfig(
        jurisdiction=child.jurisdiction,
        parent=parent.jurisdiction,
        abbreviations=merged_abbrevs,
        agency_names=merged_agencies,
        entity_types=merged_types,
        rules=parent.rules + child.rules,
        stop_words=parent.stop_words | child.stop_words,
    )

LocalizationRule dataclass

A single localization transformation rule.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
33
34
35
36
37
38
39
40
@dataclass
class LocalizationRule:
    """A single localization transformation rule."""

    pattern: str  # Text to match (case-insensitive)
    replacement: str  # Replacement text
    is_regex: bool = False  # Whether pattern is a regex
    context: str | None = None  # Optional context (e.g., "agency", "school")

Snfei dataclass

A validated SNFEI (64-character lowercase hex string).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass(frozen=True)
class Snfei:
    """A validated SNFEI (64-character lowercase hex string)."""

    value: str

    def __post_init__(self) -> None:
        """Validate SNFEI format after initialization."""
        if len(self.value) != 64:
            raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
        if not all(c in "0123456789abcdef" for c in self.value):
            raise ValueError("SNFEI must be lowercase hex")

    def __str__(self) -> str:
        """Return string representation of SNFEI."""
        return self.value

    def __repr__(self) -> str:
        """Return abbreviated representation of SNFEI."""
        return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"

    def as_str(self) -> str:
        """Return the hash value (for API compatibility)."""
        return self.value

    def short(self, length: int = 12) -> str:
        """Return a shortened version for display."""
        return self.value[:length]
__post_init__
__post_init__() -> None

Validate SNFEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
32
33
34
35
36
37
def __post_init__(self) -> None:
    """Validate SNFEI format after initialization."""
    if len(self.value) != 64:
        raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
    if not all(c in "0123456789abcdef" for c in self.value):
        raise ValueError("SNFEI must be lowercase hex")
__repr__
__repr__() -> str

Return abbreviated representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
43
44
45
def __repr__(self) -> str:
    """Return abbreviated representation of SNFEI."""
    return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"
__str__
__str__() -> str

Return string representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
39
40
41
def __str__(self) -> str:
    """Return string representation of SNFEI."""
    return self.value
as_str
as_str() -> str

Return the hash value (for API compatibility).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
47
48
49
def as_str(self) -> str:
    """Return the hash value (for API compatibility)."""
    return self.value
short
short(length: int = 12) -> str

Return a shortened version for display.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
51
52
53
def short(self, length: int = 12) -> str:
    """Return a shortened version for display."""
    return self.value[:length]

SnfeiResult dataclass

Result of SNFEI generation with confidence metadata.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@dataclass
class SnfeiResult:
    """Result of SNFEI generation with confidence metadata."""

    snfei: Snfei
    canonical: CanonicalInput
    confidence_score: float  # 0.0 to 1.0
    tier: int  # 1, 2, or 3
    fields_used: list  # Which fields contributed

    def to_dict(self) -> dict:
        """Convert result to dictionary for serialization."""
        return {
            "snfei": self.snfei.value,
            "confidence_score": self.confidence_score,
            "tier": self.tier,
            "fields_used": self.fields_used,
            "canonical": {
                "legal_name_normalized": self.canonical.legal_name_normalized,
                "address_normalized": self.canonical.address_normalized,
                "country_code": self.canonical.country_code,
                "registration_date": self.canonical.registration_date,
            },
        }
to_dict
to_dict() -> dict

Convert result to dictionary for serialization.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def to_dict(self) -> dict:
    """Convert result to dictionary for serialization."""
    return {
        "snfei": self.snfei.value,
        "confidence_score": self.confidence_score,
        "tier": self.tier,
        "fields_used": self.fields_used,
        "canonical": {
            "legal_name_normalized": self.canonical.legal_name_normalized,
            "address_normalized": self.canonical.address_normalized,
            "country_code": self.canonical.country_code,
            "registration_date": self.canonical.registration_date,
        },
    }

apply_localization

apply_localization(name: str, jurisdiction: str) -> str

Apply localization transforms to a name.

This is the Localization Functor L.

Parameters:

Name Type Description Default
name str

Raw entity name.

required
jurisdiction str

Jurisdiction code (e.g., "US/CA").

required

Returns:

Type Description
str

Name with jurisdiction-specific transforms applied.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def apply_localization(name: str, jurisdiction: str) -> str:
    """Apply localization transforms to a name.

    This is the Localization Functor L.

    Args:
        name: Raw entity name.
        jurisdiction: Jurisdiction code (e.g., "US/CA").

    Returns:
        Name with jurisdiction-specific transforms applied.
    """
    config = get_localization_config(jurisdiction)
    return config.apply_to_name(name)

build_canonical_input

build_canonical_input(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> CanonicalInput

Build a canonical input structure from raw entity data.

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional street address.

None
registration_date str | None

Optional registration/formation date.

None

Returns:

Type Description
CanonicalInput

CanonicalInput with all fields normalized.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def build_canonical_input(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> CanonicalInput:
    """Build a canonical input structure from raw entity data.

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional street address.
        registration_date: Optional registration/formation date.

    Returns:
        CanonicalInput with all fields normalized.
    """
    return CanonicalInput(
        legal_name_normalized=normalize_legal_name(legal_name),
        address_normalized=normalize_address(address) if address else None,
        country_code=country_code.upper(),
        registration_date=normalize_registration_date(registration_date)
        if registration_date
        else None,
    )

compute_snfei

compute_snfei(canonical: CanonicalInput) -> Snfei

Compute SNFEI from canonical input.

Parameters:

Name Type Description Default
canonical CanonicalInput

Normalized input structure.

required

Returns:

Type Description
Snfei

Computed SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
56
57
58
59
60
61
62
63
64
65
66
67
def compute_snfei(canonical: CanonicalInput) -> Snfei:
    """Compute SNFEI from canonical input.

    Args:
        canonical: Normalized input structure.

    Returns:
        Computed SNFEI.
    """
    hash_input = canonical.to_hash_string()
    hash_bytes = hashlib.sha256(hash_input.encode("utf-8")).hexdigest().lower()
    return Snfei(hash_bytes)

generate_snfei

generate_snfei(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> tuple[Snfei, CanonicalInput]

Generate an SNFEI from raw entity attributes.

This is the main entry point for SNFEI generation. It applies the Normalizing Functor to all inputs before hashing.

Parameters:

Name Type Description Default
legal_name str

Raw legal name from source system.

required
country_code str

ISO 3166-1 alpha-2 country code (e.g., "US", "CA").

required
address str | None

Optional primary street address.

None
registration_date str | None

Optional formation/registration date.

None

Returns:

Type Description
tuple[Snfei, CanonicalInput]

Tuple of (SNFEI, CanonicalInput) for verification.

Example

snfei, inputs = generate_snfei( ... legal_name="Springfield Unified Sch. Dist., Inc.", ... country_code="US", ... address="123 Main St., Suite 100", ... registration_date="01/15/1985", ... ) print(snfei) a1b2c3d4... print(inputs.legal_name_normalized) springfield unified school district incorporated

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def generate_snfei(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> tuple[Snfei, CanonicalInput]:
    """Generate an SNFEI from raw entity attributes.

    This is the main entry point for SNFEI generation. It applies the
    Normalizing Functor to all inputs before hashing.

    Args:
        legal_name: Raw legal name from source system.
        country_code: ISO 3166-1 alpha-2 country code (e.g., "US", "CA").
        address: Optional primary street address.
        registration_date: Optional formation/registration date.

    Returns:
        Tuple of (SNFEI, CanonicalInput) for verification.

    Example:
        >>> snfei, inputs = generate_snfei(
        ...     legal_name="Springfield Unified Sch. Dist., Inc.",
        ...     country_code="US",
        ...     address="123 Main St., Suite 100",
        ...     registration_date="01/15/1985",
        ... )
        >>> print(snfei)
        a1b2c3d4...
        >>> print(inputs.legal_name_normalized)
        springfield unified school district incorporated
    """
    canonical = build_canonical_input(
        legal_name=legal_name,
        country_code=country_code,
        address=address,
        registration_date=registration_date,
    )
    snfei = compute_snfei(canonical)
    return snfei, canonical

generate_snfei_simple

generate_snfei_simple(
    legal_name: str,
    country_code: str,
    address: str | None = None,
) -> str

Generate SNFEI as a simple hex string.

Convenience function that returns just the hash value.

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional primary street address.

None

Returns:

Type Description
str

64-character lowercase hex SNFEI string.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def generate_snfei_simple(
    legal_name: str,
    country_code: str,
    address: str | None = None,
) -> str:
    """Generate SNFEI as a simple hex string.

    Convenience function that returns just the hash value.

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional primary street address.

    Returns:
        64-character lowercase hex SNFEI string.
    """
    snfei, _ = generate_snfei(legal_name, country_code, address)
    return snfei.value

generate_snfei_with_confidence

generate_snfei_with_confidence(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
    lei: str | None = None,
    sam_uei: str | None = None,
) -> SnfeiResult

Generate SNFEI with confidence scoring and tier classification.

Tier Classification: - Tier 1: Entity has LEI (global identifier) - confidence 1.0 - Tier 2: Entity has SAM UEI (federal identifier) - confidence 0.95 - Tier 3: Entity uses SNFEI (computed hash) - confidence varies

Tier 3 Confidence Scoring: - Base: 0.5 (name + country only) - +0.2 if address is provided - +0.2 if registration_date is provided - +0.1 if name is reasonably long (>3 words)

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional street address.

None
registration_date str | None

Optional registration date.

None
lei str | None

Optional LEI (Legal Entity Identifier).

None
sam_uei str | None

Optional SAM.gov Unique Entity Identifier.

None

Returns:

Type Description
SnfeiResult

SnfeiResult with SNFEI, confidence score, and metadata.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def generate_snfei_with_confidence(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
    lei: str | None = None,
    sam_uei: str | None = None,
) -> SnfeiResult:
    """Generate SNFEI with confidence scoring and tier classification.

    Tier Classification:
    - Tier 1: Entity has LEI (global identifier) - confidence 1.0
    - Tier 2: Entity has SAM UEI (federal identifier) - confidence 0.95
    - Tier 3: Entity uses SNFEI (computed hash) - confidence varies

    Tier 3 Confidence Scoring:
    - Base: 0.5 (name + country only)
    - +0.2 if address is provided
    - +0.2 if registration_date is provided
    - +0.1 if name is reasonably long (>3 words)

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional street address.
        registration_date: Optional registration date.
        lei: Optional LEI (Legal Entity Identifier).
        sam_uei: Optional SAM.gov Unique Entity Identifier.

    Returns:
        SnfeiResult with SNFEI, confidence score, and metadata.
    """
    fields_used = ["legal_name", "country_code"]

    # Tier 1: LEI available
    if lei and len(lei) == 20:
        canonical = build_canonical_input(legal_name, country_code, address, registration_date)
        # For Tier 1, we still compute SNFEI but confidence is 1.0
        snfei = compute_snfei(canonical)
        return SnfeiResult(
            snfei=snfei,
            canonical=canonical,
            confidence_score=1.0,
            tier=1,
            fields_used=["lei"] + fields_used,
        )

    # Tier 2: SAM UEI available
    if sam_uei and len(sam_uei) == 12:
        canonical = build_canonical_input(legal_name, country_code, address, registration_date)
        snfei = compute_snfei(canonical)
        return SnfeiResult(
            snfei=snfei,
            canonical=canonical,
            confidence_score=0.95,
            tier=2,
            fields_used=["sam_uei"] + fields_used,
        )

    # Tier 3: Compute SNFEI from attributes
    canonical = build_canonical_input(legal_name, country_code, address, registration_date)
    snfei = compute_snfei(canonical)

    # Calculate confidence score
    confidence = 0.5  # Base score

    if address:
        fields_used.append("address")
        confidence += 0.2

    if registration_date:
        fields_used.append("registration_date")
        confidence += 0.2

    # Bonus for longer, more specific names
    word_count = len(canonical.legal_name_normalized.split())
    if word_count > 3:
        confidence += 0.1

    # Cap at 0.9 for Tier 3
    confidence = min(confidence, 0.9)

    return SnfeiResult(
        snfei=snfei,
        canonical=canonical,
        confidence_score=round(confidence, 2),
        tier=3,
        fields_used=fields_used,
    )

get_localization_config

get_localization_config(
    jurisdiction: str,
) -> LocalizationConfig

Get localization config for a jurisdiction (convenience function).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
436
437
438
def get_localization_config(jurisdiction: str) -> LocalizationConfig:
    """Get localization config for a jurisdiction (convenience function)."""
    return _registry.get_config(jurisdiction)

normalize_address

normalize_address(
    address: str, remove_secondary: bool = True
) -> str

Normalize a street address for SNFEI hashing.

Pipeline: 1. Lowercase 2. ASCII transliteration 3. Remove secondary unit designators (apt, suite, etc.) 4. Remove punctuation 5. Expand postal abbreviations 6. Collapse whitespace

Parameters:

Name Type Description Default
address str

Raw street address.

required
remove_secondary bool

Whether to remove apartment/suite numbers.

True

Returns:

Type Description
str

Normalized address string.

Example

normalize_address("123 N. Main St., Suite 400") "123 north main street"

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def normalize_address(
    address: str,
    remove_secondary: bool = True,
) -> str:
    """Normalize a street address for SNFEI hashing.

    Pipeline:
    1. Lowercase
    2. ASCII transliteration
    3. Remove secondary unit designators (apt, suite, etc.)
    4. Remove punctuation
    5. Expand postal abbreviations
    6. Collapse whitespace

    Args:
        address: Raw street address.
        remove_secondary: Whether to remove apartment/suite numbers.

    Returns:
        Normalized address string.

    Example:
        >>> normalize_address("123 N. Main St., Suite 400")
        "123 north main street"
    """
    if not address:
        return ""

    # 1. Lowercase
    text = address.lower()

    # 2. ASCII transliteration
    text = _to_ascii(text)

    # 3. Remove secondary unit designators
    if remove_secondary:
        for pattern in SECONDARY_UNIT_PATTERNS:
            text = re.sub(pattern, "", text, flags=re.IGNORECASE)

    # 4. Remove punctuation
    text = _remove_punctuation(text)

    # 5. Collapse whitespace first
    text = _collapse_whitespace(text)

    # 6. Expand postal abbreviations
    tokens = text.split()
    expanded = []
    for token in tokens:
        if token in US_ADDRESS_EXPANSIONS:
            expanded.append(US_ADDRESS_EXPANSIONS[token])
        else:
            expanded.append(token)
    text = " ".join(expanded)

    # 7. Final trim
    return text.strip()
normalize_legal_name(
    name: str,
    remove_stop_words: bool = True,
    preserve_initial_stop: bool = False,
) -> str

Apply the universal normalization pipeline to a legal name.

Pipeline (in order): 1. Convert to lowercase 2. ASCII transliteration 3. Remove punctuation 4. Collapse whitespace 5. Expand abbreviations 6. Remove stop words (optional) 7. Final trim

Parameters:

Name Type Description Default
name str

Raw legal name from source system.

required
remove_stop_words bool

Whether to filter out stop words.

True
preserve_initial_stop bool

If True, preserve stop word at start of name.

False

Returns:

Type Description
str

Normalized name suitable for SNFEI hashing.

Example

normalize_legal_name("The Springfield Unified Sch. Dist., Inc.") "springfield unified school district incorporated"

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def normalize_legal_name(
    name: str,
    remove_stop_words: bool = True,
    preserve_initial_stop: bool = False,
) -> str:
    """Apply the universal normalization pipeline to a legal name.

    Pipeline (in order):
    1. Convert to lowercase
    2. ASCII transliteration
    3. Remove punctuation
    4. Collapse whitespace
    5. Expand abbreviations
    6. Remove stop words (optional)
    7. Final trim

    Args:
        name: Raw legal name from source system.
        remove_stop_words: Whether to filter out stop words.
        preserve_initial_stop: If True, preserve stop word at start of name.

    Returns:
        Normalized name suitable for SNFEI hashing.

    Example:
        >>> normalize_legal_name("The Springfield Unified Sch. Dist., Inc.")
        "springfield unified school district incorporated"
    """
    if not name:
        return ""

    # 1. Lowercase
    text = name.lower()

    # 2. ASCII transliteration
    text = _to_ascii(text)

    # 3. Remove punctuation
    text = _remove_punctuation(text)

    # 4. Collapse whitespace
    text = _collapse_whitespace(text)

    # 5. Expand abbreviations
    text = _expand_abbreviations(text)

    # 6. Remove stop words
    if remove_stop_words:
        text = _remove_stop_words(text, preserve_initial=preserve_initial_stop)

    # 7. Final collapse and trim
    return _collapse_whitespace(text)

normalize_registration_date

normalize_registration_date(date_str: str) -> str | None

Normalize a registration date to ISO 8601 format.

Returns None if date cannot be parsed.

Parameters:

Name Type Description Default
date_str str

Date string in various formats.

required

Returns:

Type Description
str | None

ISO 8601 date string (YYYY-MM-DD) or None.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def normalize_registration_date(date_str: str) -> str | None:
    """Normalize a registration date to ISO 8601 format.

    Returns None if date cannot be parsed.

    Args:
        date_str: Date string in various formats.

    Returns:
        ISO 8601 date string (YYYY-MM-DD) or None.
    """
    if not date_str:
        return None

    # Remove extra whitespace
    date_str = date_str.strip()

    # Try common date patterns

    patterns = [
        # ISO format
        (r"^(\d{4})-(\d{2})-(\d{2})$", "%Y-%m-%d"),
        # US format
        (r"^(\d{1,2})/(\d{1,2})/(\d{4})$", "%m/%d/%Y"),
        (r"^(\d{1,2})-(\d{1,2})-(\d{4})$", "%m-%d-%Y"),
        # European format
        (r"^(\d{1,2})/(\d{1,2})/(\d{4})$", "%d/%m/%Y"),
        # Year only
        (r"^(\d{4})$", "%Y"),
    ]

    for pattern, fmt in patterns:
        if re.match(pattern, date_str):
            try:
                if fmt == "%Y":
                    # Year only - use January 1
                    return f"{date_str}-01-01"
                dt = datetime.strptime(date_str, fmt)
                return dt.strftime("%Y-%m-%d")
            except ValueError:
                continue

    return None

generator

SNFEI Hash Generation.

This module computes the final SNFEI (Sub-National Federated Entity Identifier) from normalized entity attributes.

The SNFEI formula

SNFEI = SHA256(Concatenate[ legal_name_normalized, address_normalized, country_code, registration_date ])

All inputs must pass through the Normalizing Functor before hashing.

Snfei dataclass

A validated SNFEI (64-character lowercase hex string).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@dataclass(frozen=True)
class Snfei:
    """A validated SNFEI (64-character lowercase hex string)."""

    value: str

    def __post_init__(self) -> None:
        """Validate SNFEI format after initialization."""
        if len(self.value) != 64:
            raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
        if not all(c in "0123456789abcdef" for c in self.value):
            raise ValueError("SNFEI must be lowercase hex")

    def __str__(self) -> str:
        """Return string representation of SNFEI."""
        return self.value

    def __repr__(self) -> str:
        """Return abbreviated representation of SNFEI."""
        return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"

    def as_str(self) -> str:
        """Return the hash value (for API compatibility)."""
        return self.value

    def short(self, length: int = 12) -> str:
        """Return a shortened version for display."""
        return self.value[:length]
__post_init__
__post_init__() -> None

Validate SNFEI format after initialization.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
32
33
34
35
36
37
def __post_init__(self) -> None:
    """Validate SNFEI format after initialization."""
    if len(self.value) != 64:
        raise ValueError(f"SNFEI must be 64 characters, got {len(self.value)}")
    if not all(c in "0123456789abcdef" for c in self.value):
        raise ValueError("SNFEI must be lowercase hex")
__repr__
__repr__() -> str

Return abbreviated representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
43
44
45
def __repr__(self) -> str:
    """Return abbreviated representation of SNFEI."""
    return f"Snfei('{self.value[:8]}...{self.value[-8:]}')"
__str__
__str__() -> str

Return string representation of SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
39
40
41
def __str__(self) -> str:
    """Return string representation of SNFEI."""
    return self.value
as_str
as_str() -> str

Return the hash value (for API compatibility).

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
47
48
49
def as_str(self) -> str:
    """Return the hash value (for API compatibility)."""
    return self.value
short
short(length: int = 12) -> str

Return a shortened version for display.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
51
52
53
def short(self, length: int = 12) -> str:
    """Return a shortened version for display."""
    return self.value[:length]
SnfeiResult dataclass

Result of SNFEI generation with confidence metadata.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
@dataclass
class SnfeiResult:
    """Result of SNFEI generation with confidence metadata."""

    snfei: Snfei
    canonical: CanonicalInput
    confidence_score: float  # 0.0 to 1.0
    tier: int  # 1, 2, or 3
    fields_used: list  # Which fields contributed

    def to_dict(self) -> dict:
        """Convert result to dictionary for serialization."""
        return {
            "snfei": self.snfei.value,
            "confidence_score": self.confidence_score,
            "tier": self.tier,
            "fields_used": self.fields_used,
            "canonical": {
                "legal_name_normalized": self.canonical.legal_name_normalized,
                "address_normalized": self.canonical.address_normalized,
                "country_code": self.canonical.country_code,
                "registration_date": self.canonical.registration_date,
            },
        }
to_dict
to_dict() -> dict

Convert result to dictionary for serialization.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def to_dict(self) -> dict:
    """Convert result to dictionary for serialization."""
    return {
        "snfei": self.snfei.value,
        "confidence_score": self.confidence_score,
        "tier": self.tier,
        "fields_used": self.fields_used,
        "canonical": {
            "legal_name_normalized": self.canonical.legal_name_normalized,
            "address_normalized": self.canonical.address_normalized,
            "country_code": self.canonical.country_code,
            "registration_date": self.canonical.registration_date,
        },
    }
compute_snfei
compute_snfei(canonical: CanonicalInput) -> Snfei

Compute SNFEI from canonical input.

Parameters:

Name Type Description Default
canonical CanonicalInput

Normalized input structure.

required

Returns:

Type Description
Snfei

Computed SNFEI.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
56
57
58
59
60
61
62
63
64
65
66
67
def compute_snfei(canonical: CanonicalInput) -> Snfei:
    """Compute SNFEI from canonical input.

    Args:
        canonical: Normalized input structure.

    Returns:
        Computed SNFEI.
    """
    hash_input = canonical.to_hash_string()
    hash_bytes = hashlib.sha256(hash_input.encode("utf-8")).hexdigest().lower()
    return Snfei(hash_bytes)
generate_snfei
generate_snfei(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> tuple[Snfei, CanonicalInput]

Generate an SNFEI from raw entity attributes.

This is the main entry point for SNFEI generation. It applies the Normalizing Functor to all inputs before hashing.

Parameters:

Name Type Description Default
legal_name str

Raw legal name from source system.

required
country_code str

ISO 3166-1 alpha-2 country code (e.g., "US", "CA").

required
address str | None

Optional primary street address.

None
registration_date str | None

Optional formation/registration date.

None

Returns:

Type Description
tuple[Snfei, CanonicalInput]

Tuple of (SNFEI, CanonicalInput) for verification.

Example

snfei, inputs = generate_snfei( ... legal_name="Springfield Unified Sch. Dist., Inc.", ... country_code="US", ... address="123 Main St., Suite 100", ... registration_date="01/15/1985", ... ) print(snfei) a1b2c3d4... print(inputs.legal_name_normalized) springfield unified school district incorporated

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def generate_snfei(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> tuple[Snfei, CanonicalInput]:
    """Generate an SNFEI from raw entity attributes.

    This is the main entry point for SNFEI generation. It applies the
    Normalizing Functor to all inputs before hashing.

    Args:
        legal_name: Raw legal name from source system.
        country_code: ISO 3166-1 alpha-2 country code (e.g., "US", "CA").
        address: Optional primary street address.
        registration_date: Optional formation/registration date.

    Returns:
        Tuple of (SNFEI, CanonicalInput) for verification.

    Example:
        >>> snfei, inputs = generate_snfei(
        ...     legal_name="Springfield Unified Sch. Dist., Inc.",
        ...     country_code="US",
        ...     address="123 Main St., Suite 100",
        ...     registration_date="01/15/1985",
        ... )
        >>> print(snfei)
        a1b2c3d4...
        >>> print(inputs.legal_name_normalized)
        springfield unified school district incorporated
    """
    canonical = build_canonical_input(
        legal_name=legal_name,
        country_code=country_code,
        address=address,
        registration_date=registration_date,
    )
    snfei = compute_snfei(canonical)
    return snfei, canonical
generate_snfei_simple
generate_snfei_simple(
    legal_name: str,
    country_code: str,
    address: str | None = None,
) -> str

Generate SNFEI as a simple hex string.

Convenience function that returns just the hash value.

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional primary street address.

None

Returns:

Type Description
str

64-character lowercase hex SNFEI string.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def generate_snfei_simple(
    legal_name: str,
    country_code: str,
    address: str | None = None,
) -> str:
    """Generate SNFEI as a simple hex string.

    Convenience function that returns just the hash value.

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional primary street address.

    Returns:
        64-character lowercase hex SNFEI string.
    """
    snfei, _ = generate_snfei(legal_name, country_code, address)
    return snfei.value
generate_snfei_with_confidence
generate_snfei_with_confidence(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
    lei: str | None = None,
    sam_uei: str | None = None,
) -> SnfeiResult

Generate SNFEI with confidence scoring and tier classification.

Tier Classification: - Tier 1: Entity has LEI (global identifier) - confidence 1.0 - Tier 2: Entity has SAM UEI (federal identifier) - confidence 0.95 - Tier 3: Entity uses SNFEI (computed hash) - confidence varies

Tier 3 Confidence Scoring: - Base: 0.5 (name + country only) - +0.2 if address is provided - +0.2 if registration_date is provided - +0.1 if name is reasonably long (>3 words)

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional street address.

None
registration_date str | None

Optional registration date.

None
lei str | None

Optional LEI (Legal Entity Identifier).

None
sam_uei str | None

Optional SAM.gov Unique Entity Identifier.

None

Returns:

Type Description
SnfeiResult

SnfeiResult with SNFEI, confidence score, and metadata.

Source code in src/python/src/civic_exchange_protocol/snfei/generator.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
def generate_snfei_with_confidence(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
    lei: str | None = None,
    sam_uei: str | None = None,
) -> SnfeiResult:
    """Generate SNFEI with confidence scoring and tier classification.

    Tier Classification:
    - Tier 1: Entity has LEI (global identifier) - confidence 1.0
    - Tier 2: Entity has SAM UEI (federal identifier) - confidence 0.95
    - Tier 3: Entity uses SNFEI (computed hash) - confidence varies

    Tier 3 Confidence Scoring:
    - Base: 0.5 (name + country only)
    - +0.2 if address is provided
    - +0.2 if registration_date is provided
    - +0.1 if name is reasonably long (>3 words)

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional street address.
        registration_date: Optional registration date.
        lei: Optional LEI (Legal Entity Identifier).
        sam_uei: Optional SAM.gov Unique Entity Identifier.

    Returns:
        SnfeiResult with SNFEI, confidence score, and metadata.
    """
    fields_used = ["legal_name", "country_code"]

    # Tier 1: LEI available
    if lei and len(lei) == 20:
        canonical = build_canonical_input(legal_name, country_code, address, registration_date)
        # For Tier 1, we still compute SNFEI but confidence is 1.0
        snfei = compute_snfei(canonical)
        return SnfeiResult(
            snfei=snfei,
            canonical=canonical,
            confidence_score=1.0,
            tier=1,
            fields_used=["lei"] + fields_used,
        )

    # Tier 2: SAM UEI available
    if sam_uei and len(sam_uei) == 12:
        canonical = build_canonical_input(legal_name, country_code, address, registration_date)
        snfei = compute_snfei(canonical)
        return SnfeiResult(
            snfei=snfei,
            canonical=canonical,
            confidence_score=0.95,
            tier=2,
            fields_used=["sam_uei"] + fields_used,
        )

    # Tier 3: Compute SNFEI from attributes
    canonical = build_canonical_input(legal_name, country_code, address, registration_date)
    snfei = compute_snfei(canonical)

    # Calculate confidence score
    confidence = 0.5  # Base score

    if address:
        fields_used.append("address")
        confidence += 0.2

    if registration_date:
        fields_used.append("registration_date")
        confidence += 0.2

    # Bonus for longer, more specific names
    word_count = len(canonical.legal_name_normalized.split())
    if word_count > 3:
        confidence += 0.1

    # Cap at 0.9 for Tier 3
    confidence = min(confidence, 0.9)

    return SnfeiResult(
        snfei=snfei,
        canonical=canonical,
        confidence_score=round(confidence, 2),
        tier=3,
        fields_used=fields_used,
    )

localization

Localization Functor: Jurisdiction-Specific Transformations.

This module loads and applies jurisdiction-specific normalization rules BEFORE the universal Normalizing Functor is applied.

The Localization Functor L transforms raw local data into a canonical intermediate form that the universal normalizer can process:

L: RawLocal  IntermediateCanonical
N: IntermediateCanonical  FinalCanonical

SNFEI = Hash(N(L(raw_data)))
Directory Structure

/localization/ base.yaml # Default/fallback rules us/ base.yaml # US-wide rules ca.yaml # California-specific ny.yaml # New York-specific ca/ base.yaml # Canada-wide rules on.yaml # Ontario-specific qc.yaml # Quebec-specific

LocalizationConfig dataclass

Configuration for a specific jurisdiction.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
@dataclass
class LocalizationConfig:
    """Configuration for a specific jurisdiction."""

    jurisdiction: str  # e.g., "US/CA", "CA/ON"
    parent: str | None  # Parent jurisdiction for inheritance

    # Transformation maps
    abbreviations: dict[str, str] = field(default_factory=dict)
    agency_names: dict[str, str] = field(default_factory=dict)
    entity_types: dict[str, str] = field(default_factory=dict)

    # Additional rules
    rules: list[LocalizationRule] = field(default_factory=list)

    # Stop words specific to this jurisdiction
    stop_words: set[str] = field(default_factory=set)

    def apply_to_name(self, name: str) -> str:
        """Apply jurisdiction-specific transformations to a name.

        Order of application:
        1. Agency name expansions
        2. Abbreviation expansions
        3. Entity type standardization
        4. Custom rules
        """
        result = name.lower()

        # 1. Agency names (exact match, case-insensitive)
        for abbrev, full in self.agency_names.items():
            # Word boundary matching
            import re

            pattern = r"\b" + re.escape(abbrev.lower()) + r"\b"
            result = re.sub(pattern, full.lower(), result)

        # 2. Abbreviations
        tokens = result.split()
        expanded = []
        for token in tokens:
            if token in self.abbreviations:
                expanded.append(self.abbreviations[token].lower())
            else:
                expanded.append(token)
        result = " ".join(expanded)

        # 3. Entity types
        for local_type, canonical_type in self.entity_types.items():
            import re

            pattern = r"\b" + re.escape(local_type.lower()) + r"\b"
            result = re.sub(pattern, canonical_type.lower(), result)

        # 4. Custom rules
        for rule in self.rules:
            if rule.is_regex:
                import re

                result = re.sub(rule.pattern, rule.replacement, result, flags=re.IGNORECASE)
            else:
                result = result.replace(rule.pattern.lower(), rule.replacement.lower())

        return result
apply_to_name
apply_to_name(name: str) -> str

Apply jurisdiction-specific transformations to a name.

Order of application: 1. Agency name expansions 2. Abbreviation expansions 3. Entity type standardization 4. Custom rules

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def apply_to_name(self, name: str) -> str:
    """Apply jurisdiction-specific transformations to a name.

    Order of application:
    1. Agency name expansions
    2. Abbreviation expansions
    3. Entity type standardization
    4. Custom rules
    """
    result = name.lower()

    # 1. Agency names (exact match, case-insensitive)
    for abbrev, full in self.agency_names.items():
        # Word boundary matching
        import re

        pattern = r"\b" + re.escape(abbrev.lower()) + r"\b"
        result = re.sub(pattern, full.lower(), result)

    # 2. Abbreviations
    tokens = result.split()
    expanded = []
    for token in tokens:
        if token in self.abbreviations:
            expanded.append(self.abbreviations[token].lower())
        else:
            expanded.append(token)
    result = " ".join(expanded)

    # 3. Entity types
    for local_type, canonical_type in self.entity_types.items():
        import re

        pattern = r"\b" + re.escape(local_type.lower()) + r"\b"
        result = re.sub(pattern, canonical_type.lower(), result)

    # 4. Custom rules
    for rule in self.rules:
        if rule.is_regex:
            import re

            result = re.sub(rule.pattern, rule.replacement, result, flags=re.IGNORECASE)
        else:
            result = result.replace(rule.pattern.lower(), rule.replacement.lower())

    return result
LocalizationRegistry

Registry for loading and caching localization configurations.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
class LocalizationRegistry:
    """Registry for loading and caching localization configurations."""

    def __init__(self, config_dir: Path | None = None):
        """Initialize the registry.

        Args:
            config_dir: Optional path to localization YAML files.
                       If None, only built-in configs are available.
        """
        self.config_dir = config_dir
        self._cache: dict[str, LocalizationConfig] = dict(BUILT_IN_CONFIGS)

    def get_config(self, jurisdiction: str) -> LocalizationConfig:
        """Get localization config for a jurisdiction.

        Falls back through parent jurisdictions if specific config not found.
        Merges child config with parent config for inheritance.

        Args:
            jurisdiction: Jurisdiction code (e.g., "us/ca", "ca/on").
                         Case-insensitive - will be normalized to lowercase.

        Returns:
            LocalizationConfig for the jurisdiction (merged with parent).
        """
        # Normalize to lowercase
        jurisdiction = jurisdiction.lower()

        # Check if we have a merged config cached
        cache_key = f"_merged_{jurisdiction}"
        if cache_key in self._cache:
            return self._cache[cache_key]

        # Get the base config for this jurisdiction
        if jurisdiction in self._cache:
            config = self._cache[jurisdiction]
        elif self.config_dir:
            config = self._load_yaml(jurisdiction)
            if config:
                self._cache[jurisdiction] = config
            else:
                config = None
        else:
            config = None

        # If no config found, fall back to parent
        if config is None:
            if "/" in jurisdiction:
                parent = jurisdiction.rsplit("/", 1)[0]
                return self.get_config(parent)
            # Return empty config as last resort
            return LocalizationConfig(jurisdiction=jurisdiction, parent=None)

        # If config has a parent, merge with parent config
        if config.parent:
            parent_config = self.get_config(config.parent)
            merged = self.merge_configs(config, parent_config)
            self._cache[cache_key] = merged
            return merged

        return config

    def _load_yaml(self, jurisdiction: str) -> LocalizationConfig | None:
        """Load config from YAML file.

        Expected paths:
            - {config_dir}/{country}/base.yaml for country-level (e.g., US, CA)
            - {config_dir}/{country}/{region}.yaml for region-level (e.g., US/CA, CA/ON)
        """
        if not self.config_dir:
            return None

        # Determine the YAML file path
        if "/" in jurisdiction:
            # Region-level: US/CA -> US/CA.yaml
            parts = jurisdiction.split("/")
            yaml_path = self.config_dir / parts[0] / f"{parts[1]}.yaml"
        else:
            # Country-level: US -> US/base.yaml
            yaml_path = self.config_dir / jurisdiction / "base.yaml"

        if not yaml_path.exists():
            return None

        try:
            with yaml_path.open("r", encoding="utf-8") as f:
                data = yaml.safe_load(f)

            if not data:
                return None

            # Parse rules if present
            rules = []
            for rule_data in data.get("rules", []):
                rules.append(
                    LocalizationRule(
                        pattern=rule_data.get("pattern", ""),
                        replacement=rule_data.get("replacement", ""),
                        is_regex=rule_data.get("is_regex", False),
                        context=rule_data.get("context"),
                    )
                )

            return LocalizationConfig(
                jurisdiction=data.get("jurisdiction", jurisdiction),
                parent=data.get("parent"),
                abbreviations=data.get("abbreviations", {}),
                agency_names=data.get("agency_names", {}),
                entity_types=data.get("entity_types", {}),
                rules=rules,
                stop_words=set(data.get("stop_words", [])),
            )
        except Exception as e:
            # Log error but don't crash
            print(f"Warning: Failed to load localization YAML {yaml_path}: {e}")
            return None

    def merge_configs(
        self, child: LocalizationConfig, parent: LocalizationConfig
    ) -> LocalizationConfig:
        """Merge child config with parent (child overrides parent)."""
        merged_abbrevs = dict(parent.abbreviations)
        merged_abbrevs.update(child.abbreviations)

        merged_agencies = dict(parent.agency_names)
        merged_agencies.update(child.agency_names)

        merged_types = dict(parent.entity_types)
        merged_types.update(child.entity_types)

        return LocalizationConfig(
            jurisdiction=child.jurisdiction,
            parent=parent.jurisdiction,
            abbreviations=merged_abbrevs,
            agency_names=merged_agencies,
            entity_types=merged_types,
            rules=parent.rules + child.rules,
            stop_words=parent.stop_words | child.stop_words,
        )
__init__
__init__(config_dir: Path | None = None)

Initialize the registry.

Parameters:

Name Type Description Default
config_dir Path | None

Optional path to localization YAML files. If None, only built-in configs are available.

None
Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
269
270
271
272
273
274
275
276
277
def __init__(self, config_dir: Path | None = None):
    """Initialize the registry.

    Args:
        config_dir: Optional path to localization YAML files.
                   If None, only built-in configs are available.
    """
    self.config_dir = config_dir
    self._cache: dict[str, LocalizationConfig] = dict(BUILT_IN_CONFIGS)
get_config
get_config(jurisdiction: str) -> LocalizationConfig

Get localization config for a jurisdiction.

Falls back through parent jurisdictions if specific config not found. Merges child config with parent config for inheritance.

Parameters:

Name Type Description Default
jurisdiction str

Jurisdiction code (e.g., "us/ca", "ca/on"). Case-insensitive - will be normalized to lowercase.

required

Returns:

Type Description
LocalizationConfig

LocalizationConfig for the jurisdiction (merged with parent).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
def get_config(self, jurisdiction: str) -> LocalizationConfig:
    """Get localization config for a jurisdiction.

    Falls back through parent jurisdictions if specific config not found.
    Merges child config with parent config for inheritance.

    Args:
        jurisdiction: Jurisdiction code (e.g., "us/ca", "ca/on").
                     Case-insensitive - will be normalized to lowercase.

    Returns:
        LocalizationConfig for the jurisdiction (merged with parent).
    """
    # Normalize to lowercase
    jurisdiction = jurisdiction.lower()

    # Check if we have a merged config cached
    cache_key = f"_merged_{jurisdiction}"
    if cache_key in self._cache:
        return self._cache[cache_key]

    # Get the base config for this jurisdiction
    if jurisdiction in self._cache:
        config = self._cache[jurisdiction]
    elif self.config_dir:
        config = self._load_yaml(jurisdiction)
        if config:
            self._cache[jurisdiction] = config
        else:
            config = None
    else:
        config = None

    # If no config found, fall back to parent
    if config is None:
        if "/" in jurisdiction:
            parent = jurisdiction.rsplit("/", 1)[0]
            return self.get_config(parent)
        # Return empty config as last resort
        return LocalizationConfig(jurisdiction=jurisdiction, parent=None)

    # If config has a parent, merge with parent config
    if config.parent:
        parent_config = self.get_config(config.parent)
        merged = self.merge_configs(config, parent_config)
        self._cache[cache_key] = merged
        return merged

    return config
merge_configs
merge_configs(
    child: LocalizationConfig, parent: LocalizationConfig
) -> LocalizationConfig

Merge child config with parent (child overrides parent).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
def merge_configs(
    self, child: LocalizationConfig, parent: LocalizationConfig
) -> LocalizationConfig:
    """Merge child config with parent (child overrides parent)."""
    merged_abbrevs = dict(parent.abbreviations)
    merged_abbrevs.update(child.abbreviations)

    merged_agencies = dict(parent.agency_names)
    merged_agencies.update(child.agency_names)

    merged_types = dict(parent.entity_types)
    merged_types.update(child.entity_types)

    return LocalizationConfig(
        jurisdiction=child.jurisdiction,
        parent=parent.jurisdiction,
        abbreviations=merged_abbrevs,
        agency_names=merged_agencies,
        entity_types=merged_types,
        rules=parent.rules + child.rules,
        stop_words=parent.stop_words | child.stop_words,
    )
LocalizationRule dataclass

A single localization transformation rule.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
33
34
35
36
37
38
39
40
@dataclass
class LocalizationRule:
    """A single localization transformation rule."""

    pattern: str  # Text to match (case-insensitive)
    replacement: str  # Replacement text
    is_regex: bool = False  # Whether pattern is a regex
    context: str | None = None  # Optional context (e.g., "agency", "school")
apply_localization
apply_localization(name: str, jurisdiction: str) -> str

Apply localization transforms to a name.

This is the Localization Functor L.

Parameters:

Name Type Description Default
name str

Raw entity name.

required
jurisdiction str

Jurisdiction code (e.g., "US/CA").

required

Returns:

Type Description
str

Name with jurisdiction-specific transforms applied.

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def apply_localization(name: str, jurisdiction: str) -> str:
    """Apply localization transforms to a name.

    This is the Localization Functor L.

    Args:
        name: Raw entity name.
        jurisdiction: Jurisdiction code (e.g., "US/CA").

    Returns:
        Name with jurisdiction-specific transforms applied.
    """
    config = get_localization_config(jurisdiction)
    return config.apply_to_name(name)
get_localization_config
get_localization_config(
    jurisdiction: str,
) -> LocalizationConfig

Get localization config for a jurisdiction (convenience function).

Source code in src/python/src/civic_exchange_protocol/snfei/localization.py
436
437
438
def get_localization_config(jurisdiction: str) -> LocalizationConfig:
    """Get localization config for a jurisdiction (convenience function)."""
    return _registry.get_config(jurisdiction)

normalizer

CEP Core Linker: The Normalizing Functor.

This module implements the universal normalization pipeline that transforms entity attributes into hash-ready canonical form for SNFEI generation.

The architecture follows the Category Theory foundation: - Localization Functor: Jurisdiction-specific transforms (YAML-driven) - Normalizing Functor: Universal normalization steps (this module) - SNFEI Hash: Final SHA-256 computation

Directory Structure

/core-linker/ normalizer.py # Universal normalization (this file) snfei.py # SNFEI hash generation address.py # Address normalization /localization/ US/ # US state-specific rules CA.yaml NY.yaml CA/ # Canada province-specific rules ON.yaml QC.yaml base.yaml # Fallback rules

Mathematical Foundation

The Normalizing Functor N transforms the category of Raw Entity Data into the category of Canonical Entity Data:

N: RawEntity → CanonicalEntity

Where N preserves identity (same entity always maps to same canonical form) and composition (N(L(x)) = N ∘ L(x) where L is the localization functor).

CanonicalInput dataclass

Normalized input for SNFEI hashing.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
@dataclass
class CanonicalInput:
    """Normalized input for SNFEI hashing."""

    legal_name_normalized: str
    address_normalized: str | None
    country_code: str
    registration_date: str | None

    def to_hash_string(self) -> str:
        """Generate the concatenated string for hashing.

        Format:
            legal_name_normalized|address_normalized|country_code|registration_date

        Empty/None fields are included as empty strings to maintain
        consistent field positions.
        """
        parts = [
            self.legal_name_normalized,
            self.address_normalized or "",
            self.country_code,
            self.registration_date or "",
        ]
        return "|".join(parts)

    def to_hash_string_v2(self) -> str:
        """Alternative format that omits empty fields.

        This produces shorter strings but requires all implementations
        to handle optional fields identically.
        """
        parts = [self.legal_name_normalized]
        if self.address_normalized:
            parts.append(self.address_normalized)
        parts.append(self.country_code)
        if self.registration_date:
            parts.append(self.registration_date)
        return "|".join(parts)
to_hash_string
to_hash_string() -> str

Generate the concatenated string for hashing.

Format

legal_name_normalized|address_normalized|country_code|registration_date

Empty/None fields are included as empty strings to maintain consistent field positions.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def to_hash_string(self) -> str:
    """Generate the concatenated string for hashing.

    Format:
        legal_name_normalized|address_normalized|country_code|registration_date

    Empty/None fields are included as empty strings to maintain
    consistent field positions.
    """
    parts = [
        self.legal_name_normalized,
        self.address_normalized or "",
        self.country_code,
        self.registration_date or "",
    ]
    return "|".join(parts)
to_hash_string_v2
to_hash_string_v2() -> str

Alternative format that omits empty fields.

This produces shorter strings but requires all implementations to handle optional fields identically.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
580
581
582
583
584
585
586
587
588
589
590
591
592
def to_hash_string_v2(self) -> str:
    """Alternative format that omits empty fields.

    This produces shorter strings but requires all implementations
    to handle optional fields identically.
    """
    parts = [self.legal_name_normalized]
    if self.address_normalized:
        parts.append(self.address_normalized)
    parts.append(self.country_code)
    if self.registration_date:
        parts.append(self.registration_date)
    return "|".join(parts)
build_canonical_input
build_canonical_input(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> CanonicalInput

Build a canonical input structure from raw entity data.

Parameters:

Name Type Description Default
legal_name str

Raw legal name.

required
country_code str

ISO 3166-1 alpha-2 country code.

required
address str | None

Optional street address.

None
registration_date str | None

Optional registration/formation date.

None

Returns:

Type Description
CanonicalInput

CanonicalInput with all fields normalized.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
def build_canonical_input(
    legal_name: str,
    country_code: str,
    address: str | None = None,
    registration_date: str | None = None,
) -> CanonicalInput:
    """Build a canonical input structure from raw entity data.

    Args:
        legal_name: Raw legal name.
        country_code: ISO 3166-1 alpha-2 country code.
        address: Optional street address.
        registration_date: Optional registration/formation date.

    Returns:
        CanonicalInput with all fields normalized.
    """
    return CanonicalInput(
        legal_name_normalized=normalize_legal_name(legal_name),
        address_normalized=normalize_address(address) if address else None,
        country_code=country_code.upper(),
        registration_date=normalize_registration_date(registration_date)
        if registration_date
        else None,
    )
normalize_address
normalize_address(
    address: str, remove_secondary: bool = True
) -> str

Normalize a street address for SNFEI hashing.

Pipeline: 1. Lowercase 2. ASCII transliteration 3. Remove secondary unit designators (apt, suite, etc.) 4. Remove punctuation 5. Expand postal abbreviations 6. Collapse whitespace

Parameters:

Name Type Description Default
address str

Raw street address.

required
remove_secondary bool

Whether to remove apartment/suite numbers.

True

Returns:

Type Description
str

Normalized address string.

Example

normalize_address("123 N. Main St., Suite 400") "123 north main street"

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def normalize_address(
    address: str,
    remove_secondary: bool = True,
) -> str:
    """Normalize a street address for SNFEI hashing.

    Pipeline:
    1. Lowercase
    2. ASCII transliteration
    3. Remove secondary unit designators (apt, suite, etc.)
    4. Remove punctuation
    5. Expand postal abbreviations
    6. Collapse whitespace

    Args:
        address: Raw street address.
        remove_secondary: Whether to remove apartment/suite numbers.

    Returns:
        Normalized address string.

    Example:
        >>> normalize_address("123 N. Main St., Suite 400")
        "123 north main street"
    """
    if not address:
        return ""

    # 1. Lowercase
    text = address.lower()

    # 2. ASCII transliteration
    text = _to_ascii(text)

    # 3. Remove secondary unit designators
    if remove_secondary:
        for pattern in SECONDARY_UNIT_PATTERNS:
            text = re.sub(pattern, "", text, flags=re.IGNORECASE)

    # 4. Remove punctuation
    text = _remove_punctuation(text)

    # 5. Collapse whitespace first
    text = _collapse_whitespace(text)

    # 6. Expand postal abbreviations
    tokens = text.split()
    expanded = []
    for token in tokens:
        if token in US_ADDRESS_EXPANSIONS:
            expanded.append(US_ADDRESS_EXPANSIONS[token])
        else:
            expanded.append(token)
    text = " ".join(expanded)

    # 7. Final trim
    return text.strip()
normalize_legal_name(
    name: str,
    remove_stop_words: bool = True,
    preserve_initial_stop: bool = False,
) -> str

Apply the universal normalization pipeline to a legal name.

Pipeline (in order): 1. Convert to lowercase 2. ASCII transliteration 3. Remove punctuation 4. Collapse whitespace 5. Expand abbreviations 6. Remove stop words (optional) 7. Final trim

Parameters:

Name Type Description Default
name str

Raw legal name from source system.

required
remove_stop_words bool

Whether to filter out stop words.

True
preserve_initial_stop bool

If True, preserve stop word at start of name.

False

Returns:

Type Description
str

Normalized name suitable for SNFEI hashing.

Example

normalize_legal_name("The Springfield Unified Sch. Dist., Inc.") "springfield unified school district incorporated"

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def normalize_legal_name(
    name: str,
    remove_stop_words: bool = True,
    preserve_initial_stop: bool = False,
) -> str:
    """Apply the universal normalization pipeline to a legal name.

    Pipeline (in order):
    1. Convert to lowercase
    2. ASCII transliteration
    3. Remove punctuation
    4. Collapse whitespace
    5. Expand abbreviations
    6. Remove stop words (optional)
    7. Final trim

    Args:
        name: Raw legal name from source system.
        remove_stop_words: Whether to filter out stop words.
        preserve_initial_stop: If True, preserve stop word at start of name.

    Returns:
        Normalized name suitable for SNFEI hashing.

    Example:
        >>> normalize_legal_name("The Springfield Unified Sch. Dist., Inc.")
        "springfield unified school district incorporated"
    """
    if not name:
        return ""

    # 1. Lowercase
    text = name.lower()

    # 2. ASCII transliteration
    text = _to_ascii(text)

    # 3. Remove punctuation
    text = _remove_punctuation(text)

    # 4. Collapse whitespace
    text = _collapse_whitespace(text)

    # 5. Expand abbreviations
    text = _expand_abbreviations(text)

    # 6. Remove stop words
    if remove_stop_words:
        text = _remove_stop_words(text, preserve_initial=preserve_initial_stop)

    # 7. Final collapse and trim
    return _collapse_whitespace(text)
normalize_registration_date
normalize_registration_date(date_str: str) -> str | None

Normalize a registration date to ISO 8601 format.

Returns None if date cannot be parsed.

Parameters:

Name Type Description Default
date_str str

Date string in various formats.

required

Returns:

Type Description
str | None

ISO 8601 date string (YYYY-MM-DD) or None.

Source code in src/python/src/civic_exchange_protocol/snfei/normalizer.py
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
def normalize_registration_date(date_str: str) -> str | None:
    """Normalize a registration date to ISO 8601 format.

    Returns None if date cannot be parsed.

    Args:
        date_str: Date string in various formats.

    Returns:
        ISO 8601 date string (YYYY-MM-DD) or None.
    """
    if not date_str:
        return None

    # Remove extra whitespace
    date_str = date_str.strip()

    # Try common date patterns

    patterns = [
        # ISO format
        (r"^(\d{4})-(\d{2})-(\d{2})$", "%Y-%m-%d"),
        # US format
        (r"^(\d{1,2})/(\d{1,2})/(\d{4})$", "%m/%d/%Y"),
        (r"^(\d{1,2})-(\d{1,2})-(\d{4})$", "%m-%d-%Y"),
        # European format
        (r"^(\d{1,2})/(\d{1,2})/(\d{4})$", "%d/%m/%Y"),
        # Year only
        (r"^(\d{4})$", "%Y"),
    ]

    for pattern, fmt in patterns:
        if re.match(pattern, date_str):
            try:
                if fmt == "%Y":
                    # Year only - use January 1
                    return f"{date_str}-01-01"
                dt = datetime.strptime(date_str, fmt)
                return dt.strftime("%Y-%m-%d")
            except ValueError:
                continue

    return None