data_canon.core
data_canon.core.dataclass
Data validation functions for travel survey data using Pydantic models.
CANONICAL_MODELS
module-attribute
CANONICAL_MODELS: dict[str, type[BaseModel]] = {
"households": survey_models.HouseholdModel,
"persons": survey_models.PersonModel,
"days": survey_models.PersonDayModel,
"unlinked_trips": survey_models.UnlinkedTripModel,
"linked_trips": survey_models.LinkedTripModel,
"tours": survey_models.TourModel,
"joint_trips": survey_models.JointTripModel,
"joint_tours": survey_models.JointTourModel,
"habitual_locations": survey_models.HabitualLocationModel,
"habitual_location_days": survey_models.HabitualLocationDayModel,
"household_weights": weighting_models.HouseholdWeightingModel,
}
CanonicalData
dataclass
Canonical data structure for travel survey data with validation.
Use the validate() method to validate specific tables.
households
class-attribute
instance-attribute
households: pl.DataFrame | None = None
persons
class-attribute
instance-attribute
persons: pl.DataFrame | None = None
days
class-attribute
instance-attribute
days: pl.DataFrame | None = None
unlinked_trips
class-attribute
instance-attribute
unlinked_trips: pl.DataFrame | None = None
linked_trips
class-attribute
instance-attribute
linked_trips: pl.DataFrame | None = None
tours
class-attribute
instance-attribute
tours: pl.DataFrame | None = None
joint_trips
class-attribute
instance-attribute
joint_trips: pl.DataFrame | None = None
joint_tours
class-attribute
instance-attribute
joint_tours: pl.DataFrame | None = None
habitual_locations
class-attribute
instance-attribute
habitual_locations: pl.DataFrame | None = None
habitual_location_days
class-attribute
instance-attribute
habitual_location_days: pl.DataFrame | None = None
household_weights
class-attribute
instance-attribute
household_weights: pl.DataFrame | None = None
models
class-attribute
instance-attribute
models: dict[str, type[BaseModel]] = field(
default_factory=lambda: dict(CANONICAL_MODELS)
)
generated_columns
class-attribute
instance-attribute
generated_columns: dict[str, dict[str, GeneratedColumn]] = field(
default_factory=dict, repr=False
)
custom_validators
class-attribute
instance-attribute
custom_validators: dict[str, list[Callable]] = field(
default_factory=lambda: {
table: (list(validators))
for table, validators in (CUSTOM_VALIDATORS.items())
}
)
TABLE_NAMES
class-attribute
instance-attribute
TABLE_NAMES: list[str] = field(
default_factory=lambda: [
"households",
"persons",
"days",
"unlinked_trips",
"linked_trips",
"joint_trips",
"tours",
"joint_tours",
"habitual_locations",
"habitual_location_days",
"household_weights",
],
repr=False,
)
register_generated_columns
register_generated_columns(
table: str,
columns: Iterable[str] | Mapping[str, str | GeneratedColumn],
) -> None
Record columns a step generated whose names come from configuration.
add_zone_ids emits {prefix}_{zone_name}, the imputation step
emits {column}_preimputed, and cascade_completeness emits one
column per usability profile, so the names differ per project and cannot
be declared as model fields. They are still ours -- computed, named in
config, and wanted in the delivered output -- so a step that creates them
says so here rather than leaving a consumer to guess from the name.
Pass a mapping to describe them as well. A model field carries its description to the codebook; a generated column has nowhere else to say what it means, so a column whose name a project chose can still explain itself. Passing a bare iterable records the names with no description.
A mapping may give a GeneratedColumn instead of a string, to
declare the constraints a model field would have carried. Without that a
generated column is delivered unchecked.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table
|
str
|
Canonical table the columns were added to. |
required |
columns
|
Iterable[str] | Mapping[str, str | GeneratedColumn]
|
Column names, or column name -> description or
|
required |
public_columns
public_columns(table: str) -> set[str]
Columns of table the pipeline stands behind: declared or generated.
describe_generated
describe_generated(table: str) -> dict[str, str]
Descriptions for table's generated columns, empty string where none.
as_dict
as_dict() -> dict[str, pl.DataFrame | None]
Return all canonical tables as a dict (including None entries).
as_dict_non_null
as_dict_non_null() -> dict[str, pl.DataFrame]
Return only non-None canonical tables as a dict.
add_models
add_models(new_models: dict[str, type[BaseModel]]) -> None
Add or override data models for validation.
Also adds the new tables as canonical data attributes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_models
|
dict[str, type[BaseModel]]
|
Dictionary of table name to Pydantic model class |
required |
validate
validate(table_name: str, step: str | None = None) -> None
Validate a table through all validation layers.
Runs validation in this order: 1. Column constraints (uniqueness) 2. Foreign key constraints 3. Row-level Pydantic validation (step-aware if step provided) 4. Custom user-registered validators
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
table_name
|
str
|
Name of the table to validate |
required |
step
|
str | None
|
Pipeline step name for step-aware validation. If None, validates all fields strictly. |
None
|
Raises:
| Type | Description |
|---|---|
DataValidationError
|
If any validation check fails |
register_validator
register_validator(*table_names: str) -> Callable
Register a custom validator on one or more tables.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*table_names
|
str
|
One or more table names to register validator on |
()
|
Returns:
| Type | Description |
|---|---|
Callable
|
Decorator function |
Example
@data.register_validator("tours") def check_tours(tours: pl.DataFrame) -> list[str]: errors = [] # Check logic return errors
@data.register_validator("tours", "linked_trips") def check_consistency( tours: pl.DataFrame, linked_trips: pl.DataFrame ) -> list[str]: # Multi-table check return []
data_canon.core.exceptions
Validation exceptions for canonical survey data.
DataValidationError
dataclass
Structured validation error with context.
Attributes:
| Name | Type | Description |
|---|---|---|
table |
str
|
Name of the table being validated |
rule |
str
|
Name of the validation rule that failed |
message |
str
|
Human-readable error description |
row_id |
int | None
|
Optional row identifier for row-level errors |
column |
str | None
|
Optional column name for column-level errors |
table
instance-attribute
table: str
rule
instance-attribute
rule: str
message
instance-attribute
message: str
row_id
class-attribute
instance-attribute
row_id: int | None = None
column
class-attribute
instance-attribute
column: str | None = None
data_canon.core.labeled_enum
Base class for labeled enumerations with values and human-readable labels.
This module provides the LabeledEnum base class for creating enumerations that store both integer values and descriptive labels, with support for canonical field names for pydantic model mapping.
LabeledEnumMeta
Metaclass for LabeledEnum that reserves canonical_field_name.
canonical_field_name
class-attribute
instance-attribute
canonical_field_name: str | None = None
field_description
class-attribute
instance-attribute
field_description: str | None = None
LabeledEnum
Base class for enumerations with values and labels.
Each enum member is defined as a tuple of (value, label): MEMBER_NAME = (1, "Descriptive Label") or MEMBER_NAME = ("some_string", "Descriptive Label")
The enum provides: - value: The integer or string value - label: The human-readable label - field_name: The canonical field name - description: The field description
Class Methods: - from_value(val): Look up an enum member by its value - from_label(label): Look up an enum member by its label - get_field_name(): Get the canonical field name for the enum - get_description(): Get the field description for the enum
Example
class Gender(LabeledEnum): canonical_field_name = "gender" field_description = "Respondent's gender identity"
MALE = (1, "Male")
FEMALE = (2, "Female")
member = Gender.MALE print(member.value) # 1 print(member.label) # "Male" print(member.field_name) # "gender" print(member.description) # "Respondent's gender identity"
found = Gender.from_value(1) # Returns Gender.MALE found = Gender.from_label("Female") # Returns Gender.FEMALE
label
property
label: str
Get the human-readable label for this enum member.
field_name
property
field_name: str | None
Get the canonical field name for this enum.
Returns the canonical_field_name class attribute if defined, otherwise returns None.
description
property
description: str | None
Get the field description for this enum.
Returns the field_description class attribute if defined, otherwise returns None.
from_value
classmethod
from_value(value: int | str, strict: bool = True) -> LabeledEnum
from_value(
value: int | str, strict: bool = False
) -> Optional[LabeledEnum]
from_value(
value: int | str, strict: bool = True
) -> Optional[LabeledEnum]
Look up an enum member by its value.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
int | str
|
The integer or string value to search for |
required |
strict
|
bool
|
If True (default), raise ValueError if not found. If False, return None if not found. |
True
|
Returns:
| Type | Description |
|---|---|
Optional[LabeledEnum]
|
The enum member with the matching value, or None if not found and strict=False |
Raises:
| Type | Description |
|---|---|
ValueError
|
If value not found and strict=True |
from_label
classmethod
from_label(
label: str, strict: bool = True
) -> Optional[LabeledEnum]
Look up an enum member by its label.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
label
|
str
|
The label to search for (case-sensitive) |
required |
strict
|
bool
|
If True (default), raise ValueError if not found. If False, return None if not found. |
True
|
Returns:
| Type | Description |
|---|---|
Optional[LabeledEnum]
|
The enum member with the matching label, or None if not found |
get_field_name
classmethod
get_field_name() -> str | None
Get the canonical field name for this enum class.
Returns:
| Type | Description |
|---|---|
str | None
|
The canonical_field_name class attribute if defined, otherwise None |
get_description
classmethod
get_description() -> str | None
Get the field description for this enum class.
Returns:
| Type | Description |
|---|---|
str | None
|
The field_description class attribute if defined, otherwise None |
to_dict
classmethod
to_dict() -> dict[int | str, str]
Get a dictionary mapping enum values to labels.
Returns:
| Type | Description |
|---|---|
dict[int | str, str]
|
A dictionary where keys are enum values and values are labels |
data_canon.core.schema_field
Module for creating Pydantic fields with relational schema metadata.
schema_field
schema_field(
*,
unique: bool = False,
fk_to: str | None = None,
required_child: bool = False,
required_child_when: str | None = None,
**field_kwargs: Any
) -> Any
Create a Pydantic Field with relational schema metadata.
This is a convenience wrapper that attaches relational metadata (uniqueness, foreign keys, required children) to Pydantic fields via json_schema_extra. Workflow metadata (which steps require which fields) is managed separately by the step registry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
unique
|
bool
|
Whether this field should be unique across records. Used to validate uniqueness constraints at the table level. |
False
|
fk_to
|
str | None
|
Foreign key reference in format "parent_table.parent_column". The referenced parent column must be marked as unique. |
None
|
required_child
|
bool
|
If True, marks this FK as requiring the parent record to have at least one child record (bidirectional FK constraint). Only valid when fk_to is specified. |
False
|
required_child_when
|
str | None
|
Name of a boolean column on the parent table. Only parent rows where it is true need a child; rows where it is false are legitimately childless. A missing column or a null value still needs a child, so the constraint never weakens silently. Only valid with required_child=True. |
None
|
**field_kwargs
|
Any
|
All other Field parameters (ge, le, default, etc.) |
{}
|
Returns:
| Type | Description |
|---|---|
Any
|
Field instance with schema metadata attached |
Example
Unique primary key
person_id: int = schema_field(ge=1, unique=True)
Foreign key with required child constraint
hh_id: int = schema_field( ... ge=1, ... fk_to="households.hh_id", ... required_child=True, ... )
Self-referential foreign key
parent_tour_id: int | None = schema_field( ... ge=1, ... fk_to="tours.tour_id", ... default=None, ... )
data_canon.core.validators
Re-exports from :mod:pipeline.step_registry for backward compatibility.
All step-aware validation metadata now lives in the step registry.
Prefer importing directly from pipeline.step_registry.