"""Data models for redistricting plans and their districts.
A *plan* is a named, dated cartographic artifact: e.g. "Alabama 2023
court-interim congressional map". Plans replace each other over time —
the same state has multiple plans for the same district type within a
single decennial cycle when a court intervenes (Alabama 2022/2023,
Louisiana 2023/2024, New York 2022/2024, Georgia 2023/2024).
The existing ``CensusVintageConfig`` in this package keys boundaries by
decade ("2010 → 2010-2019"), which cannot represent a donation made in
AL-7 in March 2023 vs the same address in September 2023 (different
geometry). These models give consumers a date-resolvable shape.
This module is the foundation of the redistricting-plan resolution
system (issue #361). The matching :mod:`siege_utilities.geo.plans.registry`
provides date-based lookup; downstream PRs wire ``RDHProvider`` and
``assign_boundaries`` to use it.
"""
from __future__ import annotations
import datetime as _dt
import enum
from dataclasses import dataclass, field
from typing import Mapping, Optional, Tuple
__all__ = [
"PlanAuthority",
"PlanDistrict",
"RedistrictingPlan",
]
[docs]
class PlanAuthority(str, enum.Enum):
"""Who drew the plan — legally meaningful for citation and audit."""
#: Enacted by the state legislature (the default cycle path).
LEGISLATURE = "legislature"
#: Court-imposed map (interim or final).
COURT = "court"
#: Independent or politician redistricting commission.
COMMISSION = "commission"
#: Pre-decennial-shift baseline; used by the legacy resolver when no
#: explicit plan covers a date.
DEFAULT = "default"
[docs]
@dataclass(frozen=True)
class PlanDistrict:
"""A single district within a redistricting plan.
All boundaries fields are optional — many consumers only need the
*identity* (which plan, which district, valid when) and resolve the
geometry separately via a :class:`BoundaryProvider`.
Attributes:
state_fips: 2-character state FIPS code (e.g. ``"01"`` for Alabama).
district_type: Lowercase district class — ``"cd"``, ``"sldu"``,
``"sldl"``, ``"county_commission"``, etc. Match the keys used
by ``CensusTIGERProvider``.
district_id: District identifier *within* the plan. ``"7"`` for
AL-7, ``"01"`` for SLDU 1, etc. Stored as the string form so
leading-zero district numbers (sometimes used by states) are
preserved.
plan_name: Stable, human-readable plan identifier
(e.g. ``"AL_2023_CD_INTERIM"``). Conventional but not enforced.
authority: Who drew the plan (see :class:`PlanAuthority`).
effective_from: First date the district is in legal effect.
effective_to: Last date the district is in legal effect, **or
``None`` for an open-ended plan (the currently active one)**.
geometry_source: Optional pointer to the geometry — a URL, a local
path, an RDH dataset slug, or any string the consuming
``BoundaryProvider`` understands. ``None`` is fine if callers
only need identity resolution.
notes: Free-text annotation (e.g. court docket, statute citation).
Goes through to logs and audit trails verbatim.
"""
state_fips: str
district_type: str
district_id: str
plan_name: str
authority: PlanAuthority
effective_from: _dt.date
effective_to: Optional[_dt.date] = None
geometry_source: Optional[str] = None
notes: Optional[str] = None
def __post_init__(self) -> None:
if self.effective_to is not None and self.effective_to < self.effective_from:
raise ValueError(
f"PlanDistrict {self.plan_name}/{self.district_id}: "
f"effective_to ({self.effective_to}) is before "
f"effective_from ({self.effective_from})."
)
[docs]
def covers_date(self, when: _dt.date) -> bool:
"""Return True iff *when* falls within this district's effective span.
Half-open at the upper end: ``effective_to`` is inclusive (if the
new plan starts the next day, encode that as
``effective_from = old_to + timedelta(days=1)``). This matches how
court orders are typically written ("effective for elections on
or after DATE") and avoids the off-by-one trap of half-open
Python slices.
"""
if when < self.effective_from:
return False
if self.effective_to is not None and when > self.effective_to:
return False
return True
[docs]
@dataclass(frozen=True)
class RedistrictingPlan:
"""A full plan: a collection of districts plus plan-level metadata.
The plan is the *unit* of court orders, statutes, and commission
actions; the individual :class:`PlanDistrict` rows just decompose it
for query convenience. ``effective_from`` / ``effective_to`` on the
plan should match the values on its constituent districts.
Attributes:
plan_name: Same convention as :attr:`PlanDistrict.plan_name`.
state_fips: 2-character state FIPS.
district_type: ``"cd"``, ``"sldu"``, etc.
authority: Who drew it.
effective_from: First date the plan is in legal effect.
effective_to: Last date in effect, or ``None`` if currently active.
districts: Tuple of :class:`PlanDistrict` rows under this plan.
metadata: Free-form mapping for citation, source URLs, court
docket numbers, etc. Not used for resolution — consumers can
stuff whatever they want here.
Note: ``frozen=True`` only forbids attribute reassignment; the
``metadata`` dict remains mutable in place. Instances are therefore
*not* hashable and should be treated as value objects, never used
as dict keys or set members.
"""
plan_name: str
state_fips: str
district_type: str
authority: PlanAuthority
effective_from: _dt.date
effective_to: Optional[_dt.date] = None
districts: Tuple[PlanDistrict, ...] = field(default_factory=tuple)
metadata: Mapping[str, str] = field(default_factory=dict)
# Mutable metadata dict means dataclass.__hash__ would silently
# accept hashing and then collide unpredictably; mark unhashable.
__hash__ = None # type: ignore[assignment]
def __post_init__(self) -> None:
if self.effective_to is not None and self.effective_to < self.effective_from:
raise ValueError(
f"RedistrictingPlan {self.plan_name}: "
f"effective_to ({self.effective_to}) is before "
f"effective_from ({self.effective_from})."
)
# Validate that constituent districts are consistent.
for d in self.districts:
if d.plan_name != self.plan_name:
raise ValueError(
f"RedistrictingPlan {self.plan_name}: district "
f"{d.district_id} has plan_name {d.plan_name!r}."
)
if d.state_fips != self.state_fips:
raise ValueError(
f"RedistrictingPlan {self.plan_name}: district "
f"{d.district_id} has state_fips {d.state_fips!r}, "
f"plan has {self.state_fips!r}."
)
if d.district_type != self.district_type:
raise ValueError(
f"RedistrictingPlan {self.plan_name}: district "
f"{d.district_id} has district_type {d.district_type!r}, "
f"plan has {self.district_type!r}."
)
[docs]
def covers_date(self, when: _dt.date) -> bool:
"""Return True iff *when* falls within this plan's effective span."""
if when < self.effective_from:
return False
if self.effective_to is not None and when > self.effective_to:
return False
return True
[docs]
def district(self, district_id: str) -> Optional[PlanDistrict]:
"""Find a district within this plan by id, or ``None``."""
for d in self.districts:
if d.district_id == district_id:
return d
return None