diff --git a/.github/workflows/seed-v2-database.yml b/.github/workflows/seed-v2-database.yml index 634352775..61af8978c 100644 --- a/.github/workflows/seed-v2-database.yml +++ b/.github/workflows/seed-v2-database.yml @@ -66,3 +66,6 @@ jobs: run: uv run python scripts/publish_v2_metadata_catalog.py env: V2_DATA_WRITE_DATABASE_URL: ${{ secrets.V2_DATA_WRITE_DATABASE_URL }} + # Reads the certified UK data release, whose repository is private, + # to check each new catalog version's region filters. + HUGGING_FACE_TOKEN: ${{ secrets.PE_UK_PRIVATE_HF_READ_TOKEN }} diff --git a/.gitignore b/.gitignore index d69167e9d..18fe946f3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ gha-creds-*.json ## vscode settings /.vscode *.db.init.lock + +# coverage.py data and reports written by `make test` +.coverage +.coverage.* +coverage.xml diff --git a/changelog.d/v2plan-1f-123-region-reference-files.changed.md b/changelog.d/v2plan-1f-123-region-reference-files.changed.md new file mode 100644 index 000000000..2ec0371e0 --- /dev/null +++ b/changelog.d/v2plan-1f-123-region-reference-files.changed.md @@ -0,0 +1 @@ +Catalog publication reads regions from the Public API's checked-in reference files, one per country, instead of PolicyEngine.py's region registry. Each region takes its dataset from the certified data release and a row filter from a scoping rule: state_fips or congressional_district_geoid for US states and districts, the UK region column for UK nations, and constituency_code_oa or la_code_oa on the local-area dataset the release routes UK constituencies and local authorities to. The UK now publishes 1,016 regions, with 650 constituencies and 361 local authorities (including Ards and North Down) by ONS code and Northern Ireland as country/northern_ireland; the national regions are "United States" and "United Kingdom". US places are listed in the reference file but left out of the catalog until they can run, and the publication evidence counts them. Before it publishes a catalog version that is not stored yet, the publication command reads the HDF5 metadata of the certified datasets the filters use, from Hugging Face at their pinned revisions, and stops if a filter's column is missing; the seed workflow's publication step receives the UK data read token as HUGGING_FACE_TOKEN for this. A catalog version that is already stored keeps the regions it was published with, so region file changes take effect in the next catalog version; republishing it logs the difference in region rows instead of failing, its evidence counts the stored regions and lists the kept version under kept_regions, and every other table must still match. diff --git a/docs/engineering/uk-private-hugging-face-credential.md b/docs/engineering/uk-private-hugging-face-credential.md index 1bc1c6134..295be325c 100644 --- a/docs/engineering/uk-private-hugging-face-credential.md +++ b/docs/engineering/uk-private-hugging-face-credential.md @@ -51,6 +51,14 @@ Cloud Run candidate resolver verifies that the exact deployed revision binds `HUGGING_FACE_TOKEN` to `pe-uk-private-hf-read-token:latest`, without printing the credential. +The v2 catalog publication step of `.github/workflows/seed-v2-database.yml` +also receives the organization secret as `HUGGING_FACE_TOKEN`, step-scoped. +Before it publishes a new catalog version, the step reads, through one +authenticated Hugging Face file system, the HDF5 metadata of the certified +datasets that version's region filters use: the UK national and local-area +datasets in the private UK data repository, and the public US dataset. It +downloads only the 1 MB blocks that hold that metadata, not whole files. + The simulation-entry Cloud Run service does not receive this credential. It submits requests to Modal and does not download model data. diff --git a/docs/migration/stage-9-v2-metadata.md b/docs/migration/stage-9-v2-metadata.md index 8ee9dc4b4..3825ad81c 100644 --- a/docs/migration/stage-9-v2-metadata.md +++ b/docs/migration/stage-9-v2-metadata.md @@ -64,8 +64,14 @@ must execute in this order: Alembic chain, confirms all heads are current, and detects ungenerated model changes. 3. Remove the migration URL from the command environment. -4. Supply only `V2_DATA_WRITE_DATABASE_URL` and the same target identity, then - run `uv run python scripts/publish_v2_metadata_catalog.py`. +4. Supply only `V2_DATA_WRITE_DATABASE_URL`, the same target identity and + `HUGGING_FACE_TOKEN` (from the organization secret + `PE_UK_PRIVATE_HF_READ_TOKEN`), then run + `uv run python scripts/publish_v2_metadata_catalog.py`. Before publishing a + catalog version that is not stored yet, the command reads the certified data + artifacts its regions' filters use and stops if one cannot select a region. + It stops before reading anything if a UK version needs checking and + `HUGGING_FACE_TOKEN` is unset, because the UK data repository is private. 5. Require the command to finish successfully and retain its non-secret JSON evidence before creating the candidate revision. 6. Deploy the Cloud Run candidate with the target identity and @@ -101,20 +107,50 @@ does not reorder, deduplicate, or repair package output. A value superseded on the immediately following day therefore has equal start and end dates and is a valid one-day interval. -The US national region uses `populace_us_2024`. A US state, -congressional-district, or place region uses its PolicyEngine.py regional -alternative when one is available. Until a later reviewed change removes the -fallback, a subnational region without such an alternative uses -`populace_us_2024`. The command emits one summary warning containing only the -affected region types and counts. The UK national and country regions use -`microcosm_uk_2024_25`, the default certified by PolicyEngine.py 6.10.0; a -country filters that national file. UK constituency and local-authority -regions use the dataset PolicyEngine.py routes them to, -`microcosm_uk_2024_25_dense`, because the national file has no area codes. - -Review the fallback summary on every new PolicyEngine.py release. A changed -count may identify an upstream catalog change that needs a reviewed dataset -selection even when publication otherwise succeeds. +Regions come from the Public API's checked-in reference files, one per country +(`policyengine_api/data/v2/catalog/region_reference_files/`), not from +PolicyEngine.py's region registry. Each row lists a region's code, label, type, +parent, US state, the value that selects its households, and the v1 identifier +it replaces. A scoping rule per country and region type gives each region its +dataset from the certified data release and its filter: + +| Regions | Dataset | Filter | +| --- | --- | --- | +| US national | the release's national dataset (`populace_us_2024`) | none | +| US states and congressional districts | the release's state and district dataset | `row_filter` on `state_fips` or `congressional_district_geoid` | +| UK national | the release's national dataset (`microcosm_uk_2024_25`) | none | +| UK nations | the release's national dataset | `row_filter` on `region` | +| UK constituencies and local authorities | the release's local-area dataset (`microcosm_uk_2024_25_dense`), which carries their codes | `row_filter` on `constituency_code_oa` or `la_code_oa`, value the ONS code | + +If the release names no dataset for a US state or district, those regions use +the default dataset and the command emits one summary warning with the affected +region types and counts. A UK constituency or local authority cannot fall back, +since the national file has no area codes: a release that routes no dataset to +their type stops publication. Region types without a rule, such as US places +until the data can select them, are left out and counted in the evidence's +`omitted_region_summaries`. + +Before a new catalog version is published, the command reads the HDF5 metadata +of each dataset the row filters use, with ranged requests at the pinned +revision, and stops if a filter's column is missing from its dataset's +household columns. It does not check that a column holds rows for every +region's value. + +Review the fallback and omitted-region summaries on every new PolicyEngine.py +release. A changed count may identify an upstream catalog change that needs a +reviewed dataset selection even when publication otherwise succeeds. + +A catalog version is identified by its country and PolicyEngine.py version and +never changes once published. When the command republishes a stored version +whose staged regions differ from the stored ones, it keeps the stored regions +unchanged, prints a warning with the country, the version and the counts of +staged region rows the stored version lacks and of stored rows not staged, and +continues. Its evidence then describes what is stored: the region count is the +stored one, `kept_regions` lists the country, version, stored and staged region +counts, and the fallback and omitted-region summaries leave that country out, +since they describe staged regions that were not published. Every other table +must still match exactly. Region reference changes therefore take effect in the +next catalog version that is published. ## Validation evidence and retry @@ -123,7 +159,9 @@ Successful JSON evidence contains: - the canonical PolicyEngine.py version; - the dependency versions derived from its packaged manifest; - catalog entity counts; -- US fallback counts; +- fallback counts and omitted-region counts by country and region type, and + the stored and staged region counts of any stored version whose regions were + kept; - elapsed publication time; and - a success outcome. @@ -142,8 +180,11 @@ confirms existing simulation and report record counts are unchanged. A failed attempt rolls back all catalog changes. Retry the same artifact only after correcting the external failure. A matching retry preserves identifiers, content, and row counts. Different normalized content under the same -PolicyEngine.py version fails for operator review; it is never silently -replaced. A later PolicyEngine.py version adds a catalog version without +PolicyEngine.py version fails for operator review, with an error naming the +first table that differs; it is never silently replaced. Regions are the +exception: they come from the Public API's reference files, so a stored version +keeps the regions it was published with (see "Source and regional dataset +behavior"). A later PolicyEngine.py version adds a catalog version without deleting or modifying earlier versioned rows, including their logical datasets, regions, model descriptions, current-law IDs, and time-period options. diff --git a/policyengine_api/data/v2/catalog/certified_data_inspection.py b/policyengine_api/data/v2/catalog/certified_data_inspection.py new file mode 100644 index 000000000..58f0164a4 --- /dev/null +++ b/policyengine_api/data/v2/catalog/certified_data_inspection.py @@ -0,0 +1,140 @@ +"""Read household column names from certified HDF5 dataset files. + +The artifacts are read from Hugging Face at their pinned revisions through +ranged requests. Only the 1 MB blocks holding the HDF5 metadata and the +column-name arrays are downloaded, a small part of each file; the household +arrays themselves are never read. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, BinaryIO, Protocol, TypeVar +from urllib.parse import quote + +import h5py +from huggingface_hub import HfFileSystem + +from policyengine_api.data.v2.catalog.errors import CertifiedDataReadError + +if TYPE_CHECKING: + from collections.abc import Callable + from contextlib import AbstractContextManager + + from policyengine_api.data.v2.catalog.records import CertifiedArtifact + +HOUSEHOLD_TABLE = "household" +# PyTables names the columns a table stores together "values_block_"; their +# real names live only in pickled attributes. +VALUE_BLOCK_PREFIX = "values_block_" +TABLE_INDEX_COLUMN = "index" + +_Result = TypeVar("_Result") + + +# HDF5 metadata is read in many small, scattered pieces; caching whole blocks of +# this size keeps the number of ranged requests low. +READ_BLOCK_BYTES = 1 << 20 + + +class ArtifactFileSystem(Protocol): + """Opens a file in a Hugging Face repository at a revision for reading.""" + + def open( + self, path: str, mode: str, *, block_size: int, cache_type: str + ) -> AbstractContextManager[BinaryIO]: + """Return a readable, seekable binary file that caches blocks it reads.""" + ... + + +def huggingface_path(artifact: CertifiedArtifact) -> str: + """Return the Hugging Face file system path of an artifact at its revision.""" + prefix = "datasets/" if artifact.repo_type == "dataset" else "" + revision = quote(artifact.revision, safe="") + return f"{prefix}{artifact.repo_id}@{revision}/{artifact.path}" + + +def _text(value: object) -> str: + return value.decode("utf-8") if isinstance(value, bytes) else str(value) + + +def _is_frame(group: h5py.Group) -> bool: + return "axis0" in group or "table" in group + + +def _frame_columns(group: h5py.Group) -> frozenset[str]: + """Return the column names of one pandas data frame stored in HDF5.""" + if "axis0" in group: + return frozenset(_text(value) for value in group["axis0"][()]) + names = group["table"].dtype.names or () + if any(name.startswith(VALUE_BLOCK_PREFIX) for name in names): + msg = ( + f"household table {group.name.lstrip('/')} stores columns in value " + "blocks, whose names cannot be read without unpickling" + ) + raise CertifiedDataReadError(msg) + return frozenset(name for name in names if name != TABLE_INDEX_COLUMN) + + +def household_columns_in_hdf5(h5: h5py.File) -> frozenset[str]: + """Return the household column names of a dataset file. + + A pandas file stores the household frame directly, or one frame per year + (then only columns every year has count). A policyengine-core file stores + one top-level entry per variable. + """ + if HOUSEHOLD_TABLE not in h5: + return frozenset(h5.keys()) + household = h5[HOUSEHOLD_TABLE] + if not isinstance(household, h5py.Group): + msg = "household entry is not an HDF5 group" + raise CertifiedDataReadError(msg) + if _is_frame(household): + return _frame_columns(household) + frames = [ + entry + for entry in household.values() + if isinstance(entry, h5py.Group) and _is_frame(entry) + ] + if not frames: + msg = "household group holds no data frame" + raise CertifiedDataReadError(msg) + return frozenset.intersection(*(_frame_columns(frame) for frame in frames)) + + +def _hugging_face_file_system(token: str | None) -> ArtifactFileSystem: + return HfFileSystem(token=token) + + +class HuggingFaceDataInspector: + """Reads certified artifacts from Hugging Face with an optional read token.""" + + def __init__( + self, + *, + token: str | None, + file_system_factory: Callable[[str | None], ArtifactFileSystem] = ( + _hugging_face_file_system + ), + ) -> None: + """Build the file system once; a token of None reads public data only.""" + self._file_system = file_system_factory(token) + + def _read( + self, + artifact: CertifiedArtifact, + read: Callable[[h5py.File], _Result], + ) -> _Result: + with ( + self._file_system.open( + huggingface_path(artifact), + "rb", + block_size=READ_BLOCK_BYTES, + cache_type="blockcache", + ) as handle, + h5py.File(handle, "r") as h5, + ): + return read(h5) + + def household_columns(self, artifact: CertifiedArtifact) -> frozenset[str]: + """Return the household column names of a dataset artifact.""" + return self._read(artifact, household_columns_in_hdf5) diff --git a/policyengine_api/data/v2/catalog/errors.py b/policyengine_api/data/v2/catalog/errors.py new file mode 100644 index 000000000..e33762b81 --- /dev/null +++ b/policyengine_api/data/v2/catalog/errors.py @@ -0,0 +1,14 @@ +"""Errors raised while catalog sources are read and normalized.""" + +from __future__ import annotations + + +class CatalogExtractionError(RuntimeError): + """Raised before database access when source metadata cannot be normalized.""" + + +class CertifiedDataReadError(RuntimeError): + """Raised when a certified artifact does not have the structure expected. + + Its message names only HDF5 entries and counts, so it is safe to print. + """ diff --git a/policyengine_api/data/v2/catalog/extraction.py b/policyengine_api/data/v2/catalog/extraction.py index 75b0b213f..410340cea 100644 --- a/policyengine_api/data/v2/catalog/extraction.py +++ b/policyengine_api/data/v2/catalog/extraction.py @@ -2,18 +2,16 @@ from __future__ import annotations -import json import math import re -from collections import Counter from collections.abc import Callable, Mapping, Sequence from datetime import UTC, date, datetime, timedelta from enum import Enum from importlib import metadata as importlib_metadata -from typing import TYPE_CHECKING, Any, NamedTuple, Protocol -from urllib.parse import urlsplit +from typing import TYPE_CHECKING, Any from uuid import NAMESPACE_URL, UUID, uuid5 +from policyengine_api.data.v2.catalog.errors import CatalogExtractionError from policyengine_api.data.v2.catalog.records import ( CountryCatalog, DatasetRecord, @@ -21,12 +19,22 @@ ModelRecord, ModelVersionRecord, NormalizedCatalog, + OmittedRegionSummary, ParameterNodeRecord, ParameterRecord, ParameterValueRecord, RegionRecord, VariableRecord, ) +from policyengine_api.data.v2.catalog.region_references import ( + RegionReference, + load_region_references, +) +from policyengine_api.data.v2.catalog.region_scoping import ( + ScopedRegion, + filtered_artifacts, + scope_regions, +) if TYPE_CHECKING: from policyengine_api.json_types import JSONValue @@ -46,26 +54,10 @@ "us": tuple(range(2035, 2021, -1)), "uk": tuple(range(2024, 2031)), } -SUPPORTED_REGION_TYPES = frozenset( - { - "national", - "country", - "state", - "congressional_district", - "constituency", - "local_authority", - "city", - "place", - } -) PLACEHOLDER_VERSIONS = frozenset({"", "0", "0.0", "0.0.0", "unknown"}) DATASET_YEAR_PATTERN = re.compile(r"(?:19|20)\d{2}") -class CatalogExtractionError(RuntimeError): - """Raised before database access when source metadata cannot be normalized.""" - - def _identifier(*parts: object) -> UUID: value = "/".join(str(part) for part in parts) return uuid5(NAMESPACE_URL, f"https://api.policyengine.org/v2/catalog/{value}") @@ -99,44 +91,6 @@ def _optional_text( return value -# The catalog's region filter_value column holds at most this many characters. -MAX_REGION_FILTER_VALUE_LENGTH = 255 - - -def _serialize_region_filter_value(value: object, *, region_code: str) -> str: - """Keep scalar text or encode flat membership values in the existing column. - - Since PolicyEngine 6.2.7, the England filter names all nine English regions. The - catalog's existing text column can retain that complete list as a JSON - array without changing its schema or the representation of scalar filters. - """ - scalar_types = (str, int, float, bool) - if isinstance(value, scalar_types): - serialized = str(value) - elif ( - isinstance(value, (list, tuple)) - and value - and all(isinstance(item, scalar_types) for item in value) - ): - try: - serialized = json.dumps( - value, ensure_ascii=False, separators=(",", ":"), allow_nan=False - ) - except ValueError as error: - msg = f"region {region_code!r} has an unsupported filter value" - raise CatalogExtractionError(msg) from error - else: - msg = f"region {region_code!r} has an unsupported filter value" - raise CatalogExtractionError(msg) - if len(serialized) > MAX_REGION_FILTER_VALUE_LENGTH: - msg = ( - f"region {region_code!r} filter value exceeds " - f"{MAX_REGION_FILTER_VALUE_LENGTH} characters" - ) - raise CatalogExtractionError(msg) - return serialized - - def _type_name(value: object) -> str | None: if value is None: return None @@ -236,17 +190,6 @@ def _optional_aware_datetime(value: object, *, field_name: str) -> datetime | No return _aware_datetime(value, field_name=field_name) -def _dataset_name_from_path(path: str) -> str: - without_revision = path.split("@", maxsplit=1)[0] - parsed_path = urlsplit(without_revision).path or without_revision - filename = parsed_path.rstrip("/").rsplit("/", maxsplit=1)[-1] - for suffix in (".hdf5", ".parquet", ".csv", ".h5"): - if filename.endswith(suffix): - filename = filename[: -len(suffix)] - break - return _required_text(filename, field_name="dataset name", maximum=255) - - def _dataset_year(name: str) -> int: matches = DATASET_YEAR_PATTERN.findall(name) if not matches: @@ -492,18 +435,6 @@ def _public_named_records( return tuple(normalized) -class _RegionDatasetSelection(NamedTuple): - """The logical input datasets a country's regions select. - - Names every selected dataset, each region code's dataset, and how many - regions of each type fall back to the reviewed default dataset. - """ - - dataset_names: set[str] - region_dataset_names: dict[str, str] - fallback_counts: Counter[str] - - def _verified_model_name(country_id: str, source_model: object) -> str: """Return the public model's name once it matches the country's model.""" expected_model_name = f"policyengine-{country_id}" @@ -595,184 +526,39 @@ def _normalize_parameter_nodes( return tuple(normalized_nodes) -def _verified_default_dataset( - country_id: str, - release_manifest: object, -) -> tuple[str, object]: - """Return the reviewed default dataset and the manifest's URI for it. - - The manifest's default dataset must be the reviewed one. - """ +def _verified_default_dataset(country_id: str, release_manifest: object) -> str: + """Return the reviewed default dataset once the manifest names it as its default.""" reviewed_default = REVIEWED_DEFAULT_DATASETS[country_id] manifest_default = getattr(release_manifest, "default_dataset", None) if manifest_default != reviewed_default: msg = f"{country_id} default dataset does not match the reviewed selection" raise CatalogExtractionError(msg) - return reviewed_default, getattr(release_manifest, "default_dataset_uri", None) - - -def _verified_source_regions(country_id: str, source: object) -> tuple[Any, ...]: - """Return the country's public regions once their codes are unique and complete. - - The registry must belong to the country, and its codes must be unique and - include the national region. - """ - registry = getattr(source, "region_registry", None) - if registry is None or getattr(registry, "country_id", None) != country_id: - msg = f"{country_id} public region registry is absent" - raise CatalogExtractionError(msg) - - source_regions = tuple(registry) - region_codes = [getattr(region, "code", None) for region in source_regions] - duplicate_region_codes = sorted( - code for code, count in Counter(region_codes).items() if count > 1 - ) - if duplicate_region_codes: - msg = f"duplicate region natural keys: {duplicate_region_codes[:3]}" - raise CatalogExtractionError(msg) - if country_id not in region_codes: - msg = f"{country_id} national region is absent" - raise CatalogExtractionError(msg) - return source_regions - - -def _select_region_datasets( - country_id: str, - source_regions: tuple[Any, ...], - *, - reviewed_default: str, - default_dataset_uri: object, -) -> _RegionDatasetSelection: - """Choose each region's logical input dataset. - - A subnational region with a dataset path uses that dataset: US states and - districts, and the UK constituencies and local authorities that .py routes - to its local-area release. Every other region, such as a UK country that - filters the national file, uses the reviewed default, and the national - region may only name it. A US subnational region without a path is - counted as a fallback. - """ - selected_dataset_names: set[str] = {reviewed_default} - region_dataset_names: dict[str, str] = {} - fallback_counts: Counter[str] = Counter() - for region in source_regions: - code = _required_text( - getattr(region, "code", None), - field_name="region code", - maximum=255, - ) - region_type = _required_text( - getattr(region, "region_type", None), - field_name=f"region {code} type", - maximum=64, - ) - if region_type not in SUPPORTED_REGION_TYPES: - msg = f"region {code!r} has unsupported type {region_type!r}" - raise CatalogExtractionError(msg) - dataset_path = getattr(region, "dataset_path", None) - if region_type != "national" and dataset_path: - dataset_name = _dataset_name_from_path(dataset_path) - selected_dataset_names.add(dataset_name) - else: - dataset_name = reviewed_default - if country_id == "us" and region_type != "national" and not dataset_path: - fallback_counts[region_type] += 1 - if region_type == "national" and dataset_path not in { - None, - default_dataset_uri, - }: - msg = f"{country_id} national region does not use the reviewed dataset" - raise CatalogExtractionError(msg) - region_dataset_names[code] = dataset_name - return _RegionDatasetSelection( - dataset_names=selected_dataset_names, - region_dataset_names=region_dataset_names, - fallback_counts=fallback_counts, - ) - - -def _region_filter(strategy: object, code: str) -> tuple[str, str, str]: - """Return a filtered region's strategy type, filter field and filter value.""" - strategy_type = _required_text( - getattr(strategy, "strategy_type", None), - field_name=f"region {code} filter strategy", - maximum=64, - ) - filter_field = _required_text( - getattr(strategy, "variable_name", None), - field_name=f"region {code} filter field", - maximum=128, - ) - filter_value = _serialize_region_filter_value( - getattr(strategy, "variable_value", None), region_code=code - ) - additional_filters = getattr(strategy, "additional_filters", {}) - if additional_filters: - msg = f"region {code!r} has unsupported additional filters" - raise CatalogExtractionError(msg) - return strategy_type, filter_field, filter_value - - -class _SourceRegion(Protocol): - """The attributes of a PolicyEngine.py region that _normalize_region reads directly. - - It reads every other region attribute through getattr, with a default. - """ - - @property - def code(self) -> object: ... - - @property - def region_type(self) -> object: ... + return reviewed_default def _normalize_region( - source_region: _SourceRegion, + scoped_region: ScopedRegion, *, model_version_id: UUID, - region_dataset_ids: Mapping[str, UUID], + dataset_ids: Mapping[str, UUID], ) -> RegionRecord: - """Normalize one public region, with its filter and default dataset.""" - code = str(source_region.code) - strategy = getattr(source_region, "scoping_strategy", None) - requires_filter = bool(getattr(source_region, "requires_filter", False)) - if requires_filter: - strategy_type, filter_field, filter_value = _region_filter(strategy, code) - else: - strategy_type = None - filter_field = None - filter_value = None - + """Build one region's record from its reference, dataset and filter.""" + reference = scoped_region.reference + requires_filter = scoped_region.filter_strategy is not None return RegionRecord( - id=_identifier("region", model_version_id, code), + id=_identifier("region", model_version_id, reference.code), model_version_id=model_version_id, - default_dataset_id=region_dataset_ids[code], - code=code, - label=_required_text( - getattr(source_region, "label", None), - field_name=f"region {code} label", - maximum=255, - ), - region_type=str(source_region.region_type), + default_dataset_id=dataset_ids[scoped_region.dataset_name], + code=reference.code, + label=reference.label, + region_type=reference.region_type, requires_filter=requires_filter, - filter_field=filter_field, - filter_value=filter_value, - filter_strategy=strategy_type, - parent_code=_optional_text( - getattr(source_region, "parent_code", None), - field_name=f"region {code} parent_code", - maximum=255, - ), - state_code=_optional_text( - getattr(source_region, "state_code", None), - field_name=f"region {code} state_code", - maximum=16, - ), - state_name=_optional_text( - getattr(source_region, "state_name", None), - field_name=f"region {code} state_name", - maximum=128, - ), + filter_field=scoped_region.filter_field, + filter_value=reference.filter_value if requires_filter else None, + filter_strategy=scoped_region.filter_strategy, + parent_code=reference.parent_code, + state_code=reference.state_code, + state_name=reference.state_name, ) @@ -782,6 +568,7 @@ def _normalize_country( source: object, policyengine_version: str, expected_country_package_version: str, + region_references: tuple[RegionReference, ...], ) -> CountryCatalog: source_model = getattr(source, "model", None) model_name = _verified_model_name(country_id, source_model) @@ -814,15 +601,12 @@ def _normalize_country( ) ) - reviewed_default, default_dataset_uri = _verified_default_dataset( - country_id, release_manifest - ) - source_regions = _verified_source_regions(country_id, source) - dataset_selection = _select_region_datasets( + reviewed_default = _verified_default_dataset(country_id, release_manifest) + scoping = scope_regions( country_id, - source_regions, + region_references, + release_manifest, reviewed_default=reviewed_default, - default_dataset_uri=default_dataset_uri, ) datasets = tuple( @@ -833,20 +617,16 @@ def _normalize_country( description=f"PolicyEngine.py logical input dataset {name}", year=_dataset_year(name), ) - for name in sorted(dataset_selection.dataset_names) + for name in sorted(scoping.dataset_names) ) dataset_ids = {dataset.name: dataset.id for dataset in datasets} - region_dataset_ids = { - code: dataset_ids[name] - for code, name in dataset_selection.region_dataset_names.items() - } regions = [ _normalize_region( - source_region, + scoped_region, model_version_id=model_version_id, - region_dataset_ids=region_dataset_ids, + dataset_ids=dataset_ids, ) - for source_region in source_regions + for scoped_region in scoping.regions ] return CountryCatalog( @@ -878,8 +658,13 @@ def _normalize_country( regions=tuple(sorted(regions, key=lambda record: record.code)), fallback_summaries=tuple( FallbackSummary(region_type=region_type, count=count) - for region_type, count in sorted(dataset_selection.fallback_counts.items()) + for region_type, count in scoping.fallback_counts.items() ), + omitted_region_summaries=tuple( + OmittedRegionSummary(region_type=region_type, count=count) + for region_type, count in scoping.omitted_counts.items() + ), + certified_artifacts=filtered_artifacts(scoping), ) @@ -888,9 +673,14 @@ def extract_catalog( bundle: Mapping[str, Any], policyengine_version: str, models: Mapping[str, object], + region_references: Mapping[str, Sequence[RegionReference]], installed_version: Callable[[str], str] = importlib_metadata.version, ) -> NormalizedCatalog: - """Validate and normalize supplied PolicyEngine.py public objects.""" + """Validate and normalize supplied PolicyEngine.py public objects. + + Regions come from the supplied reference files, scoped by each country's + certified data release. + """ dependencies = _verify_bundle( bundle=bundle, policyengine_version=policyengine_version, @@ -900,6 +690,12 @@ def extract_catalog( if missing: msg = f"PolicyEngine.py public models are absent for: {', '.join(missing)}" raise CatalogExtractionError(msg) + missing = [ + country for country in SUPPORTED_COUNTRIES if country not in region_references + ] + if missing: + msg = f"region references are absent for: {', '.join(missing)}" + raise CatalogExtractionError(msg) package_map = bundle["packages"] countries = tuple( @@ -910,6 +706,7 @@ def extract_catalog( expected_country_package_version=package_map[f"policyengine-{country_id}"][ "version" ], + region_references=tuple(region_references[country_id]), ) for country_id in SUPPORTED_COUNTRIES ) @@ -921,7 +718,10 @@ def extract_catalog( def extract_installed_catalog() -> NormalizedCatalog: - """Load and normalize the installed PolicyEngine.py certified catalog.""" + """Load and normalize the installed PolicyEngine.py certified catalog. + + Regions come from the Public API's checked-in reference files. + """ import policyengine from policyengine.bundle import ( get_current_bundle, @@ -935,4 +735,8 @@ def extract_installed_catalog() -> NormalizedCatalog: "us": policyengine.us.model, "uk": policyengine.uk.model, }, + region_references={ + country_id: load_region_references(country_id) + for country_id in SUPPORTED_COUNTRIES + }, ) diff --git a/policyengine_api/data/v2/catalog/initialization.py b/policyengine_api/data/v2/catalog/initialization.py index 10efdda6f..978e40e47 100644 --- a/policyengine_api/data/v2/catalog/initialization.py +++ b/policyengine_api/data/v2/catalog/initialization.py @@ -3,12 +3,16 @@ from __future__ import annotations import json +import os import sys from typing import TYPE_CHECKING from sqlalchemy import Engine, create_engine from sqlalchemy.pool import NullPool +from policyengine_api.data.v2.catalog.certified_data_inspection import ( + HuggingFaceDataInspector, +) from policyengine_api.data.v2.catalog.extraction import ( CatalogExtractionError, extract_installed_catalog, @@ -17,6 +21,11 @@ CatalogPublicationError, PublicationEvidence, publish_catalog, + unpublished_countries, +) +from policyengine_api.data.v2.catalog.region_data_verification import ( + RegionDataVerificationError, + verify_region_data, ) from policyengine_api.data.v2.settings import ( V2ConfigurationError, @@ -25,16 +34,68 @@ ) if TYPE_CHECKING: - from collections.abc import Callable, Mapping + from collections.abc import Callable, Mapping, Sequence from policyengine_api.data.v2.catalog.records import NormalizedCatalog +# The environment variable holding the Hugging Face read token; PolicyEngine +# Core's data downloads read the same variable. +HUGGING_FACE_CREDENTIAL_VARIABLE = "HUGGING_FACE_TOKEN" +# Countries whose certified data repository needs the read token. +PRIVATE_DATA_COUNTRIES = frozenset({"uk"}) + + def build_data_write_engine(settings: V2DatabaseSettings) -> Engine: """Build an isolated, unpooled engine for one initialization attempt.""" return create_engine(settings.connection.url, poolclass=NullPool) +def verify_installed_region_data( + catalog: NormalizedCatalog, + countries: Sequence[str], + environ: Mapping[str, str] | None = None, +) -> None: + """Check the named countries' regions against their certified artifacts. + + The artifacts are read from Hugging Face with ``HUGGING_FACE_TOKEN``. + Countries whose certified data repository is private need it, so their + check fails with a configuration error before any read when it is unset. + """ + token = (os.environ if environ is None else environ).get( + HUGGING_FACE_CREDENTIAL_VARIABLE + ) + needing_token = [ + country for country in countries if country in PRIVATE_DATA_COUNTRIES + ] + if needing_token and not token: + msg = ( + f"{HUGGING_FACE_CREDENTIAL_VARIABLE} must be set to read the certified " + f"{', '.join(needing_token)} data, whose repository is private" + ) + raise V2ConfigurationError(msg) + verify_region_data( + catalog, + HuggingFaceDataInspector(token=token or None), + countries=countries, + ) + + +def verify_unpublished_region_data( + engine: Engine, + catalog: NormalizedCatalog, + environ: Mapping[str, str] | None = None, +) -> None: + """Check the regions of countries whose catalog version is not stored yet. + + Publication writes nothing for the other countries and only compares them + with what is stored, so their artifacts are not read. + """ + verify_installed_region_data( + catalog, unpublished_countries(engine, catalog), environ + ) + + def initialize_catalog( environ: Mapping[str, str] | None = None, *, @@ -43,12 +104,16 @@ def initialize_catalog( publisher: Callable[[Engine, NormalizedCatalog], PublicationEvidence] = ( publish_catalog ), + region_data_verifier: Callable[ + [Engine, NormalizedCatalog, Mapping[str, str] | None], None + ] = verify_unpublished_region_data, ) -> PublicationEvidence: - """Load only row-write settings, extract fully, then publish explicitly.""" + """Load only row-write settings, extract fully, verify, then publish explicitly.""" settings = load_v2_data_write_database_settings(environ) catalog = extractor() engine = engine_builder(settings) try: + region_data_verifier(engine, catalog, environ) return publisher(engine, catalog) finally: engine.dispose() @@ -58,6 +123,7 @@ def _error_payload(error: Exception) -> dict[str, object]: safe_errors = ( V2ConfigurationError, CatalogExtractionError, + RegionDataVerificationError, CatalogPublicationError, ) message = ( diff --git a/policyengine_api/data/v2/catalog/publication.py b/policyengine_api/data/v2/catalog/publication.py index 5e629fb05..a8951d6b5 100644 --- a/policyengine_api/data/v2/catalog/publication.py +++ b/policyengine_api/data/v2/catalog/publication.py @@ -12,6 +12,8 @@ from policyengine_api.data.v2.catalog.publication_reconciliation import ( assert_canonical_value_uniqueness, assert_country_matches, + count_region_differences, + count_stored_regions, publish_new_country, version_exists, ) @@ -34,7 +36,10 @@ if TYPE_CHECKING: from collections.abc import Callable - from policyengine_api.data.v2.catalog.records import NormalizedCatalog + from policyengine_api.data.v2.catalog.records import ( + CountryCatalog, + NormalizedCatalog, + ) # Stable application-defined PostgreSQL lock ID shared by all v2 catalog publishers. PUBLICATION_ADVISORY_LOCK_KEY = 8_629_020_026_090_001 @@ -56,6 +61,7 @@ "CatalogPublicationError", "PublicationEvidence", "publish_catalog", + "unpublished_countries", ] @@ -87,6 +93,25 @@ def _protected_row_counts(connection: Connection) -> tuple[int, int, int, int]: ) +def unpublished_countries( + engine: Engine, catalog: NormalizedCatalog +) -> tuple[str, ...]: + """Return the countries whose catalog version is not stored yet, in catalog order. + + It first requires the expected v2 revision, as publication does, so a + missing or older schema is reported as such. A read outside publication's + transaction: a version another publisher adds meanwhile is still compared + with the staged one when this one publishes. + """ + with engine.connect() as connection: + _verify_expected_revision(connection) + return tuple( + country.country_id + for country in catalog.countries + if not version_exists(connection, country) + ) + + def publish_catalog( engine: Engine, catalog: NormalizedCatalog, @@ -104,11 +129,7 @@ def publish_catalog( if checkpoint is not None: checkpoint("after_copy", connection) - existing: set[str] = set() - for country in catalog.countries: - if version_exists(connection, country): - assert_country_matches(connection, country) - existing.add(country.country_id) + existing, kept_region_summaries = _check_stored_versions(connection, catalog) for country in catalog.countries: if country.country_id not in existing: @@ -117,7 +138,11 @@ def publish_catalog( checkpoint("after_reconciliation", connection) for country in catalog.countries: - assert_country_matches(connection, country) + assert_country_matches( + connection, + country, + include_regions=country.country_id not in existing, + ) assert_canonical_value_uniqueness(connection) if _protected_row_counts(connection) != before: msg = "publication changed simulation, report, or dataset-version rows" @@ -125,18 +150,108 @@ def publish_catalog( if checkpoint is not None: checkpoint("after_validation", connection) + return _evidence(catalog, kept_region_summaries, started=started) + + +def _check_stored_versions( + connection: Connection, + catalog: NormalizedCatalog, +) -> tuple[set[str], tuple[tuple[str, str, int, int], ...]]: + """Compare each stored catalog version and return its countries and kept regions. + + A stored catalog version never changes. Its regions stay as they were + published even when the reference files differ now; every other table + must still match exactly. + """ + existing: set[str] = set() + kept_region_summaries: list[tuple[str, str, int, int]] = [] + for country in catalog.countries: + if version_exists(connection, country): + assert_country_matches(connection, country, include_regions=False) + kept = _kept_region_summary(connection, country) + if kept is not None: + kept_region_summaries.append(kept) + existing.add(country.country_id) + return existing, tuple(kept_region_summaries) + + +def _evidence( + catalog: NormalizedCatalog, + kept_region_summaries: tuple[tuple[str, str, int, int], ...], + *, + started: float, +) -> PublicationEvidence: + """Describe what is stored after publication. + + Every table of a stored version matches the staged catalog except regions + that were kept: for those countries the evidence counts the stored region + rows and reports none of the staged fallbacks or omissions, which describe + rows that were not published. + """ + kept_countries = {country_id for country_id, *_counts in kept_region_summaries} + entity_counts = catalog.entity_counts() + entity_counts["regions"] += sum( + stored - staged for _country, _version, stored, staged in kept_region_summaries + ) fallback_summaries = tuple( (country.country_id, summary.region_type, summary.count) for country in catalog.countries + if country.country_id not in kept_countries for summary in country.fallback_summaries ) _log_fallback_warning(fallback_summaries) return PublicationEvidence( policyengine_version=catalog.policyengine_version, dependency_versions=catalog.dependency_versions, - entity_counts=catalog.entity_counts(), + entity_counts=entity_counts, fallback_summaries=fallback_summaries, elapsed_seconds=time.monotonic() - started, + omitted_region_summaries=tuple( + (country.country_id, summary.region_type, summary.count) + for country in catalog.countries + if country.country_id not in kept_countries + for summary in country.omitted_region_summaries + ), + kept_region_summaries=kept_region_summaries, + ) + + +def _kept_region_summary( + connection: Connection, + country: CountryCatalog, +) -> tuple[str, str, int, int] | None: + """Return and log how a stored version's kept regions differ from the staged. + + The summary is the country, the version and the stored and staged region + counts; there is none when the regions are the same. The log names counts + only. + """ + staged_not_stored, stored_not_staged = count_region_differences(connection, country) + if not staged_not_stored and not stored_not_staged: + return None + # The seed command configures no logging handler, so the details are in the + # message itself; the attributes serve structured handlers. + LOGGER.warning( + "kept the published regions of the stored %s catalog version %s: %d staged " + "region rows are not stored and %d stored region rows are not staged", + country.country_id, + country.model_version.version, + staged_not_stored, + stored_not_staged, + extra={ + "policyengine_attributes": { + "country_id": country.country_id, + "policyengine_version": country.model_version.version, + "staged_region_rows_not_stored": staged_not_stored, + "stored_region_rows_not_staged": stored_not_staged, + } + }, + ) + return ( + country.country_id, + country.model_version.version, + count_stored_regions(connection, country), + len(country.regions), ) diff --git a/policyengine_api/data/v2/catalog/publication_reconciliation.py b/policyengine_api/data/v2/catalog/publication_reconciliation.py index 0977676c6..8a0c9c0aa 100644 --- a/policyengine_api/data/v2/catalog/publication_reconciliation.py +++ b/policyengine_api/data/v2/catalog/publication_reconciliation.py @@ -78,7 +78,8 @@ def _versioned_table_from(table: sa.Table) -> sa.Join: def _comparison_pairs( country: CountryCatalog, -) -> tuple[tuple[Select, Select], ...]: +) -> tuple[tuple[str, Select, Select], ...]: + """Return each compared table's name with its staged and stored rows.""" staged_country = STAGE_CATALOG_MODELS.c.country_id == country.country_id selected_version = sa.and_( MODELS.c.name == country.model.name, @@ -220,6 +221,22 @@ def _comparison_pairs( ) ) + return ( + ("model version", staged_model_version, actual_model_version), + ("variables", staged_variables, actual_variables), + ("parameter nodes", staged_parameter_nodes, actual_parameter_nodes), + ("parameters", staged_parameters, actual_parameters), + ("parameter values", staged_parameter_values, actual_parameter_values), + ("datasets", staged_datasets, actual_datasets), + ) + + +def _region_comparison_pair(country: CountryCatalog) -> tuple[Select, Select]: + """Return the staged and stored region rows of one country's catalog version.""" + selected_version = sa.and_( + MODELS.c.name == country.model.name, + MODEL_VERSIONS.c.version == country.model_version.version, + ) staged_regions = sa.select( STAGE_CATALOG_REGIONS.c.code, STAGE_CATALOG_REGIONS.c.label, @@ -256,15 +273,7 @@ def _comparison_pairs( .where(selected_version) ) - return ( - (staged_model_version, actual_model_version), - (staged_variables, actual_variables), - (staged_parameter_nodes, actual_parameter_nodes), - (staged_parameters, actual_parameters), - (staged_parameter_values, actual_parameter_values), - (staged_datasets, actual_datasets), - (staged_regions, actual_regions), - ) + return staged_regions, actual_regions def _sets_differ( @@ -284,18 +293,56 @@ def _sets_differ( def assert_country_matches( connection: Connection, country: CountryCatalog, + *, + include_regions: bool = True, ) -> None: """Raise CatalogPublicationError if the stored version differs from the staged one. Rows are compared as sets in both directions, so a missing row and an extra - row both fail. + row both fail; the error names the first table that differs, never its + rows. Without ``include_regions``, region rows are not compared: they come + from the Public API's reference files, which may change while the + PolicyEngine.py version stays the same. """ - for staged, actual in _comparison_pairs(country): + pairs = _comparison_pairs(country) + if include_regions: + pairs = (*pairs, ("regions", *_region_comparison_pair(country))) + for table_name, staged, actual in pairs: if _sets_differ(connection, staged, actual): - msg = f"persisted {country.country_id} catalog differs from PolicyEngine.py" + msg = ( + f"the stored {country.country_id} catalog version " + f"{country.model_version.version} differs from the staged catalog " + f"in its {table_name}" + ) raise CatalogPublicationError(msg) +def count_stored_regions(connection: Connection, country: CountryCatalog) -> int: + """Count the region rows stored for one country's catalog version.""" + _staged, actual = _region_comparison_pair(country) + return connection.execute( + sa.select(sa.func.count()).select_from(actual.subquery()) + ).scalar_one() + + +def count_region_differences( + connection: Connection, + country: CountryCatalog, +) -> tuple[int, int]: + """Count staged region rows the stored version lacks, and stored rows not staged.""" + staged, actual = _region_comparison_pair(country) + staged_not_stored = sa.except_(staged, actual).subquery() + stored_not_staged = sa.except_(actual, staged).subquery() + return ( + connection.execute( + sa.select(sa.func.count()).select_from(staged_not_stored) + ).scalar_one(), + connection.execute( + sa.select(sa.func.count()).select_from(stored_not_staged) + ).scalar_one(), + ) + + INSERT_MODEL = ( postgresql_insert(MODELS) .from_select( diff --git a/policyengine_api/data/v2/catalog/publication_types.py b/policyengine_api/data/v2/catalog/publication_types.py index 884bcef07..515232560 100644 --- a/policyengine_api/data/v2/catalog/publication_types.py +++ b/policyengine_api/data/v2/catalog/publication_types.py @@ -18,6 +18,10 @@ class PublicationEvidence: entity_counts: dict[str, int] fallback_summaries: tuple[tuple[str, str, int], ...] elapsed_seconds: float + omitted_region_summaries: tuple[tuple[str, str, int], ...] = () + # Stored catalog versions whose region rows were kept although the staged + # regions differ: country, version, stored and staged region counts. + kept_region_summaries: tuple[tuple[str, str, int, int], ...] = () def as_dict(self) -> dict[str, object]: """Return the evidence as the JSON object printed after a publication.""" @@ -34,5 +38,22 @@ def as_dict(self) -> dict[str, object]: } for country_id, region_type, count in self.fallback_summaries ], + "omitted_region_summaries": [ + { + "country_id": country_id, + "region_type": region_type, + "count": count, + } + for country_id, region_type, count in self.omitted_region_summaries + ], + "kept_regions": [ + { + "country_id": country_id, + "policyengine_version": version, + "stored_regions": stored, + "staged_regions": staged, + } + for country_id, version, stored, staged in self.kept_region_summaries + ], "elapsed_seconds": round(self.elapsed_seconds, 3), } diff --git a/policyengine_api/data/v2/catalog/records.py b/policyengine_api/data/v2/catalog/records.py index afccc4add..f05233202 100644 --- a/policyengine_api/data/v2/catalog/records.py +++ b/policyengine_api/data/v2/catalog/records.py @@ -126,11 +126,37 @@ class RegionRecord: state_name: str | None +@dataclass(frozen=True, slots=True) +class CertifiedArtifact: + """One artifact of a country's certified data release, pinned to a revision. + + ``repo_type`` is the Hugging Face repository type, ``model`` or ``dataset``. + """ + + name: str + repo_id: str + repo_type: str + path: str + revision: str + sha256: str | None + + @dataclass(frozen=True, slots=True) class FallbackSummary: - """How many US sub-national regions of one type fell back to the default dataset. + """How many sub-national regions of one type fell back to the default dataset. + + The certified data release names no dataset for their type. + """ + + region_type: str + count: int + + +@dataclass(frozen=True, slots=True) +class OmittedRegionSummary: + """How many reference regions of one type publication left out. - These regions had no dataset path of their own. + No scoping rule can select their households yet, such as US places. """ region_type: str @@ -166,6 +192,9 @@ class CountryCatalog: datasets: tuple[DatasetRecord, ...] regions: tuple[RegionRecord, ...] fallback_summaries: tuple[FallbackSummary, ...] + omitted_region_summaries: tuple[OmittedRegionSummary, ...] = () + # The release datasets the regions' row filters read. + certified_artifacts: tuple[CertifiedArtifact, ...] = () def parameter_value_batches( self, diff --git a/policyengine_api/data/v2/catalog/region_data_verification.py b/policyengine_api/data/v2/catalog/region_data_verification.py new file mode 100644 index 000000000..e7eda396a --- /dev/null +++ b/policyengine_api/data/v2/catalog/region_data_verification.py @@ -0,0 +1,126 @@ +"""Refuse to publish regions whose households the certified data cannot select. + +A row-filtered region needs its filter column among the household columns of +its dataset. +""" + +from __future__ import annotations + +from collections import defaultdict +from functools import partial +from typing import TYPE_CHECKING, Protocol, TypeVar + +from policyengine_api.data.v2.catalog.errors import CertifiedDataReadError +from policyengine_api.data.v2.catalog.region_scoping import ROW_FILTER + +if TYPE_CHECKING: + from collections.abc import Callable, Mapping, Sequence + + from policyengine_api.data.v2.catalog.records import ( + CertifiedArtifact, + CountryCatalog, + NormalizedCatalog, + ) + +# Region codes named in an error message, at most. +MAX_LISTED_REGIONS = 3 + +_Result = TypeVar("_Result") + + +class RegionDataVerificationError(RuntimeError): + """Raised when certified data cannot select a region's households. + + Also raised when an artifact cannot be read for that check. Verification + runs after the stored catalog versions are read and before anything is + published; its messages name only artifacts, columns, codes and counts. + """ + + +class CertifiedDataInspector(Protocol): + """Reads what verification needs from one certified artifact.""" + + def household_columns(self, artifact: CertifiedArtifact) -> frozenset[str]: + """Return the household column names of a dataset artifact.""" + ... + + +def _listed(codes: Sequence[str]) -> str: + shown = ", ".join(codes[:MAX_LISTED_REGIONS]) + more = len(codes) - MAX_LISTED_REGIONS + return f"{shown} and {more} more" if more > 0 else shown + + +def _noun(count: int) -> str: + return "region" if count == 1 else "regions" + + +def _read(artifact: CertifiedArtifact, read: Callable[[], _Result]) -> _Result: + """Run one read of an artifact, reporting a failure safely. + + A structure error keeps its own message; any other failure is reduced to + the artifact's name and the error type, since its text may hold a location. + """ + try: + return read() + except RegionDataVerificationError: + raise + except CertifiedDataReadError as error: + # Its message names only HDF5 entries and counts. + msg = f"the certified artifact {artifact.name} cannot be checked: {error}" + raise RegionDataVerificationError(msg) from error + except Exception as error: + # Only the artifact's name and the error type: the error text may hold + # its location, and command output names no dataset release. + msg = ( + f"could not read the certified artifact {artifact.name}: " + f"{type(error).__name__}" + ) + raise RegionDataVerificationError(msg) from error + + +def _verify_row_filters( + country: CountryCatalog, + artifacts: Mapping[str, CertifiedArtifact], + inspector: CertifiedDataInspector, +) -> None: + dataset_names = {dataset.id: dataset.name for dataset in country.datasets} + regions_by_column: dict[tuple[str, str], list[str]] = defaultdict(list) + for region in country.regions: + if region.filter_strategy == ROW_FILTER and region.filter_field: + dataset_name = dataset_names[region.default_dataset_id] + regions_by_column[dataset_name, region.filter_field].append(region.code) + columns_by_dataset: dict[str, frozenset[str]] = {} + for (dataset_name, column), codes in sorted(regions_by_column.items()): + if dataset_name not in columns_by_dataset: + artifact = artifacts[dataset_name] + columns_by_dataset[dataset_name] = _read( + artifact, + partial(inspector.household_columns, artifact), + ) + if column not in columns_by_dataset[dataset_name]: + msg = ( + f"the certified dataset {dataset_name} has no household column " + f"{column}, which {len(codes)} {country.country_id} " + f"{_noun(len(codes))} {'filters' if len(codes) == 1 else 'filter'} " + f"on ({_listed(codes)})" + ) + raise RegionDataVerificationError(msg) + + +def verify_region_data( + catalog: NormalizedCatalog, + inspector: CertifiedDataInspector, + *, + countries: Sequence[str], +) -> None: + """Check the regions of the named countries against their certified artifacts. + + Each artifact is read at most once. + """ + for country_id in countries: + country = catalog.country(country_id) + artifacts = { + artifact.name: artifact for artifact in country.certified_artifacts + } + _verify_row_filters(country, artifacts, inspector) diff --git a/policyengine_api/data/v2/catalog/region_reference_files/uk.csv b/policyengine_api/data/v2/catalog/region_reference_files/uk.csv new file mode 100644 index 000000000..e20f450de --- /dev/null +++ b/policyengine_api/data/v2/catalog/region_reference_files/uk.csv @@ -0,0 +1,1017 @@ +code,label,region_type,parent_code,state_code,state_name,filter_value,v1_code +uk,United Kingdom,national,,,,,uk +country/england,England,country,uk,,,"[""NORTH_EAST"",""NORTH_WEST"",""YORKSHIRE"",""EAST_MIDLANDS"",""WEST_MIDLANDS"",""EAST_OF_ENGLAND"",""LONDON"",""SOUTH_EAST"",""SOUTH_WEST""]",country/england +country/scotland,Scotland,country,uk,,,SCOTLAND,country/scotland +country/wales,Wales,country,uk,,,WALES,country/wales +country/northern_ireland,Northern Ireland,country,uk,,,NORTHERN_IRELAND,country/ni +constituency/E14001063,Aldershot,constituency,country/england,,,E14001063,constituency/Aldershot +constituency/E14001064,Aldridge-Brownhills,constituency,country/england,,,E14001064,constituency/Aldridge-Brownhills +constituency/E14001065,Altrincham and Sale West,constituency,country/england,,,E14001065,constituency/Altrincham and Sale West +constituency/E14001066,Amber Valley,constituency,country/england,,,E14001066,constituency/Amber Valley +constituency/E14001067,Arundel and South Downs,constituency,country/england,,,E14001067,constituency/Arundel and South Downs +constituency/E14001068,Ashfield,constituency,country/england,,,E14001068,constituency/Ashfield +constituency/E14001069,Ashford,constituency,country/england,,,E14001069,constituency/Ashford +constituency/E14001070,Ashton-under-Lyne,constituency,country/england,,,E14001070,constituency/Ashton-under-Lyne +constituency/E14001071,Aylesbury,constituency,country/england,,,E14001071,constituency/Aylesbury +constituency/E14001072,Banbury,constituency,country/england,,,E14001072,constituency/Banbury +constituency/E14001073,Barking,constituency,country/england,,,E14001073,constituency/Barking +constituency/E14001074,Barnsley North,constituency,country/england,,,E14001074,constituency/Barnsley North +constituency/E14001075,Barnsley South,constituency,country/england,,,E14001075,constituency/Barnsley South +constituency/E14001076,Barrow and Furness,constituency,country/england,,,E14001076,constituency/Barrow and Furness +constituency/E14001077,Basildon and Billericay,constituency,country/england,,,E14001077,constituency/Basildon and Billericay +constituency/E14001078,Basingstoke,constituency,country/england,,,E14001078,constituency/Basingstoke +constituency/E14001079,Bassetlaw,constituency,country/england,,,E14001079,constituency/Bassetlaw +constituency/E14001080,Bath,constituency,country/england,,,E14001080,constituency/Bath +constituency/E14001081,Battersea,constituency,country/england,,,E14001081,constituency/Battersea +constituency/E14001082,Beaconsfield,constituency,country/england,,,E14001082,constituency/Beaconsfield +constituency/E14001083,Beckenham and Penge,constituency,country/england,,,E14001083,constituency/Beckenham and Penge +constituency/E14001084,Bedford,constituency,country/england,,,E14001084,constituency/Bedford +constituency/E14001085,Bermondsey and Old Southwark,constituency,country/england,,,E14001085,constituency/Bermondsey and Old Southwark +constituency/E14001086,Bethnal Green and Stepney,constituency,country/england,,,E14001086,constituency/Bethnal Green and Stepney +constituency/E14001087,Beverley and Holderness,constituency,country/england,,,E14001087,constituency/Beverley and Holderness +constituency/E14001088,Bexhill and Battle,constituency,country/england,,,E14001088,constituency/Bexhill and Battle +constituency/E14001089,Bexleyheath and Crayford,constituency,country/england,,,E14001089,constituency/Bexleyheath and Crayford +constituency/E14001090,Bicester and Woodstock,constituency,country/england,,,E14001090,constituency/Bicester and Woodstock +constituency/E14001091,Birkenhead,constituency,country/england,,,E14001091,constituency/Birkenhead +constituency/E14001092,Birmingham Edgbaston,constituency,country/england,,,E14001092,constituency/Birmingham Edgbaston +constituency/E14001093,Birmingham Erdington,constituency,country/england,,,E14001093,constituency/Birmingham Erdington +constituency/E14001094,Birmingham Hall Green and Moseley,constituency,country/england,,,E14001094,constituency/Birmingham Hall Green and Moseley +constituency/E14001095,Birmingham Hodge Hill and Solihull North,constituency,country/england,,,E14001095,constituency/Birmingham Hodge Hill and Solihull North +constituency/E14001096,Birmingham Ladywood,constituency,country/england,,,E14001096,constituency/Birmingham Ladywood +constituency/E14001097,Birmingham Northfield,constituency,country/england,,,E14001097,constituency/Birmingham Northfield +constituency/E14001098,Birmingham Perry Barr,constituency,country/england,,,E14001098,constituency/Birmingham Perry Barr +constituency/E14001099,Birmingham Selly Oak,constituency,country/england,,,E14001099,constituency/Birmingham Selly Oak +constituency/E14001100,Birmingham Yardley,constituency,country/england,,,E14001100,constituency/Birmingham Yardley +constituency/E14001101,Bishop Auckland,constituency,country/england,,,E14001101,constituency/Bishop Auckland +constituency/E14001102,Blackburn,constituency,country/england,,,E14001102,constituency/Blackburn +constituency/E14001103,Blackley and Middleton South,constituency,country/england,,,E14001103,constituency/Blackley and Middleton South +constituency/E14001104,Blackpool North and Fleetwood,constituency,country/england,,,E14001104,constituency/Blackpool North and Fleetwood +constituency/E14001105,Blackpool South,constituency,country/england,,,E14001105,constituency/Blackpool South +constituency/E14001106,Blaydon and Consett,constituency,country/england,,,E14001106,constituency/Blaydon and Consett +constituency/E14001107,Blyth and Ashington,constituency,country/england,,,E14001107,constituency/Blyth and Ashington +constituency/E14001108,Bognor Regis and Littlehampton,constituency,country/england,,,E14001108,constituency/Bognor Regis and Littlehampton +constituency/E14001109,Bolsover,constituency,country/england,,,E14001109,constituency/Bolsover +constituency/E14001110,Bolton North East,constituency,country/england,,,E14001110,constituency/Bolton North East +constituency/E14001111,Bolton South and Walkden,constituency,country/england,,,E14001111,constituency/Bolton South and Walkden +constituency/E14001112,Bolton West,constituency,country/england,,,E14001112,constituency/Bolton West +constituency/E14001113,Bootle,constituency,country/england,,,E14001113,constituency/Bootle +constituency/E14001114,Boston and Skegness,constituency,country/england,,,E14001114,constituency/Boston and Skegness +constituency/E14001115,Bournemouth East,constituency,country/england,,,E14001115,constituency/Bournemouth East +constituency/E14001116,Bournemouth West,constituency,country/england,,,E14001116,constituency/Bournemouth West +constituency/E14001117,Bracknell,constituency,country/england,,,E14001117,constituency/Bracknell +constituency/E14001118,Bradford East,constituency,country/england,,,E14001118,constituency/Bradford East +constituency/E14001119,Bradford South,constituency,country/england,,,E14001119,constituency/Bradford South +constituency/E14001120,Bradford West,constituency,country/england,,,E14001120,constituency/Bradford West +constituency/E14001121,Braintree,constituency,country/england,,,E14001121,constituency/Braintree +constituency/E14001122,Brent East,constituency,country/england,,,E14001122,constituency/Brent East +constituency/E14001123,Brent West,constituency,country/england,,,E14001123,constituency/Brent West +constituency/E14001124,Brentford and Isleworth,constituency,country/england,,,E14001124,constituency/Brentford and Isleworth +constituency/E14001125,Brentwood and Ongar,constituency,country/england,,,E14001125,constituency/Brentwood and Ongar +constituency/E14001126,Bridgwater,constituency,country/england,,,E14001126,constituency/Bridgwater +constituency/E14001127,Bridlington and The Wolds,constituency,country/england,,,E14001127,constituency/Bridlington and The Wolds +constituency/E14001128,Brigg and Immingham,constituency,country/england,,,E14001128,constituency/Brigg and Immingham +constituency/E14001129,Brighton Kemptown and Peacehaven,constituency,country/england,,,E14001129,constituency/Brighton Kemptown and Peacehaven +constituency/E14001130,Brighton Pavilion,constituency,country/england,,,E14001130,constituency/Brighton Pavilion +constituency/E14001131,Bristol Central,constituency,country/england,,,E14001131,constituency/Bristol Central +constituency/E14001132,Bristol East,constituency,country/england,,,E14001132,constituency/Bristol East +constituency/E14001133,Bristol North East,constituency,country/england,,,E14001133,constituency/Bristol North East +constituency/E14001134,Bristol North West,constituency,country/england,,,E14001134,constituency/Bristol North West +constituency/E14001135,Bristol South,constituency,country/england,,,E14001135,constituency/Bristol South +constituency/E14001136,Broadland and Fakenham,constituency,country/england,,,E14001136,constituency/Broadland and Fakenham +constituency/E14001137,Bromley and Biggin Hill,constituency,country/england,,,E14001137,constituency/Bromley and Biggin Hill +constituency/E14001138,Bromsgrove,constituency,country/england,,,E14001138,constituency/Bromsgrove +constituency/E14001139,Broxbourne,constituency,country/england,,,E14001139,constituency/Broxbourne +constituency/E14001140,Broxtowe,constituency,country/england,,,E14001140,constituency/Broxtowe +constituency/E14001141,Buckingham and Bletchley,constituency,country/england,,,E14001141,constituency/Buckingham and Bletchley +constituency/E14001142,Burnley,constituency,country/england,,,E14001142,constituency/Burnley +constituency/E14001143,Burton and Uttoxeter,constituency,country/england,,,E14001143,constituency/Burton and Uttoxeter +constituency/E14001144,Bury North,constituency,country/england,,,E14001144,constituency/Bury North +constituency/E14001145,Bury South,constituency,country/england,,,E14001145,constituency/Bury South +constituency/E14001146,Bury St Edmunds and Stowmarket,constituency,country/england,,,E14001146,constituency/Bury St Edmunds and Stowmarket +constituency/E14001147,Calder Valley,constituency,country/england,,,E14001147,constituency/Calder Valley +constituency/E14001148,Camborne and Redruth,constituency,country/england,,,E14001148,constituency/Camborne and Redruth +constituency/E14001149,Cambridge,constituency,country/england,,,E14001149,constituency/Cambridge +constituency/E14001150,Cannock Chase,constituency,country/england,,,E14001150,constituency/Cannock Chase +constituency/E14001151,Canterbury,constituency,country/england,,,E14001151,constituency/Canterbury +constituency/E14001152,Carlisle,constituency,country/england,,,E14001152,constituency/Carlisle +constituency/E14001153,Carshalton and Wallington,constituency,country/england,,,E14001153,constituency/Carshalton and Wallington +constituency/E14001154,Castle Point,constituency,country/england,,,E14001154,constituency/Castle Point +constituency/E14001155,Central Devon,constituency,country/england,,,E14001155,constituency/Central Devon +constituency/E14001156,Central Suffolk and North Ipswich,constituency,country/england,,,E14001156,constituency/Central Suffolk and North Ipswich +constituency/E14001157,Chatham and Aylesford,constituency,country/england,,,E14001157,constituency/Chatham and Aylesford +constituency/E14001158,Cheadle,constituency,country/england,,,E14001158,constituency/Cheadle +constituency/E14001159,Chelmsford,constituency,country/england,,,E14001159,constituency/Chelmsford +constituency/E14001160,Chelsea and Fulham,constituency,country/england,,,E14001160,constituency/Chelsea and Fulham +constituency/E14001161,Cheltenham,constituency,country/england,,,E14001161,constituency/Cheltenham +constituency/E14001162,Chesham and Amersham,constituency,country/england,,,E14001162,constituency/Chesham and Amersham +constituency/E14001163,Chester North and Neston,constituency,country/england,,,E14001163,constituency/Chester North and Neston +constituency/E14001164,Chester South and Eddisbury,constituency,country/england,,,E14001164,constituency/Chester South and Eddisbury +constituency/E14001165,Chesterfield,constituency,country/england,,,E14001165,constituency/Chesterfield +constituency/E14001166,Chichester,constituency,country/england,,,E14001166,constituency/Chichester +constituency/E14001167,Chingford and Woodford Green,constituency,country/england,,,E14001167,constituency/Chingford and Woodford Green +constituency/E14001168,Chippenham,constituency,country/england,,,E14001168,constituency/Chippenham +constituency/E14001169,Chipping Barnet,constituency,country/england,,,E14001169,constituency/Chipping Barnet +constituency/E14001170,Chorley,constituency,country/england,,,E14001170,constituency/Chorley +constituency/E14001171,Christchurch,constituency,country/england,,,E14001171,constituency/Christchurch +constituency/E14001172,Cities of London and Westminster,constituency,country/england,,,E14001172,constituency/Cities of London and Westminster +constituency/E14001173,City of Durham,constituency,country/england,,,E14001173,constituency/City of Durham +constituency/E14001174,Clacton,constituency,country/england,,,E14001174,constituency/Clacton +constituency/E14001175,Clapham and Brixton Hill,constituency,country/england,,,E14001175,constituency/Clapham and Brixton Hill +constituency/E14001176,Colchester,constituency,country/england,,,E14001176,constituency/Colchester +constituency/E14001177,Colne Valley,constituency,country/england,,,E14001177,constituency/Colne Valley +constituency/E14001178,Congleton,constituency,country/england,,,E14001178,constituency/Congleton +constituency/E14001179,Corby and East Northamptonshire,constituency,country/england,,,E14001179,constituency/Corby and East Northamptonshire +constituency/E14001180,Coventry East,constituency,country/england,,,E14001180,constituency/Coventry East +constituency/E14001181,Coventry North West,constituency,country/england,,,E14001181,constituency/Coventry North West +constituency/E14001182,Coventry South,constituency,country/england,,,E14001182,constituency/Coventry South +constituency/E14001183,Cramlington and Killingworth,constituency,country/england,,,E14001183,constituency/Cramlington and Killingworth +constituency/E14001184,Crawley,constituency,country/england,,,E14001184,constituency/Crawley +constituency/E14001185,Crewe and Nantwich,constituency,country/england,,,E14001185,constituency/Crewe and Nantwich +constituency/E14001186,Croydon East,constituency,country/england,,,E14001186,constituency/Croydon East +constituency/E14001187,Croydon South,constituency,country/england,,,E14001187,constituency/Croydon South +constituency/E14001188,Croydon West,constituency,country/england,,,E14001188,constituency/Croydon West +constituency/E14001189,Dagenham and Rainham,constituency,country/england,,,E14001189,constituency/Dagenham and Rainham +constituency/E14001190,Darlington,constituency,country/england,,,E14001190,constituency/Darlington +constituency/E14001191,Dartford,constituency,country/england,,,E14001191,constituency/Dartford +constituency/E14001192,Daventry,constituency,country/england,,,E14001192,constituency/Daventry +constituency/E14001193,Derby North,constituency,country/england,,,E14001193,constituency/Derby North +constituency/E14001194,Derby South,constituency,country/england,,,E14001194,constituency/Derby South +constituency/E14001195,Derbyshire Dales,constituency,country/england,,,E14001195,constituency/Derbyshire Dales +constituency/E14001196,Dewsbury and Batley,constituency,country/england,,,E14001196,constituency/Dewsbury and Batley +constituency/E14001197,Didcot and Wantage,constituency,country/england,,,E14001197,constituency/Didcot and Wantage +constituency/E14001198,Doncaster Central,constituency,country/england,,,E14001198,constituency/Doncaster Central +constituency/E14001199,Doncaster East and the Isle of Axholme,constituency,country/england,,,E14001199,constituency/Doncaster East and the Isle of Axholme +constituency/E14001200,Doncaster North,constituency,country/england,,,E14001200,constituency/Doncaster North +constituency/E14001201,Dorking and Horley,constituency,country/england,,,E14001201,constituency/Dorking and Horley +constituency/E14001202,Dover and Deal,constituency,country/england,,,E14001202,constituency/Dover and Deal +constituency/E14001203,Droitwich and Evesham,constituency,country/england,,,E14001203,constituency/Droitwich and Evesham +constituency/E14001204,Dudley,constituency,country/england,,,E14001204,constituency/Dudley +constituency/E14001205,Dulwich and West Norwood,constituency,country/england,,,E14001205,constituency/Dulwich and West Norwood +constituency/E14001206,Dunstable and Leighton Buzzard,constituency,country/england,,,E14001206,constituency/Dunstable and Leighton Buzzard +constituency/E14001207,Ealing Central and Acton,constituency,country/england,,,E14001207,constituency/Ealing Central and Acton +constituency/E14001208,Ealing North,constituency,country/england,,,E14001208,constituency/Ealing North +constituency/E14001209,Ealing Southall,constituency,country/england,,,E14001209,constituency/Ealing Southall +constituency/E14001210,Earley and Woodley,constituency,country/england,,,E14001210,constituency/Earley and Woodley +constituency/E14001211,Easington,constituency,country/england,,,E14001211,constituency/Easington +constituency/E14001212,East Grinstead and Uckfield,constituency,country/england,,,E14001212,constituency/East Grinstead and Uckfield +constituency/E14001213,East Ham,constituency,country/england,,,E14001213,constituency/East Ham +constituency/E14001214,East Hampshire,constituency,country/england,,,E14001214,constituency/East Hampshire +constituency/E14001215,East Surrey,constituency,country/england,,,E14001215,constituency/East Surrey +constituency/E14001216,East Thanet,constituency,country/england,,,E14001216,constituency/East Thanet +constituency/E14001217,East Wiltshire,constituency,country/england,,,E14001217,constituency/East Wiltshire +constituency/E14001218,East Worthing and Shoreham,constituency,country/england,,,E14001218,constituency/East Worthing and Shoreham +constituency/E14001219,Eastbourne,constituency,country/england,,,E14001219,constituency/Eastbourne +constituency/E14001220,Eastleigh,constituency,country/england,,,E14001220,constituency/Eastleigh +constituency/E14001221,Edmonton and Winchmore Hill,constituency,country/england,,,E14001221,constituency/Edmonton and Winchmore Hill +constituency/E14001222,Ellesmere Port and Bromborough,constituency,country/england,,,E14001222,constituency/Ellesmere Port and Bromborough +constituency/E14001223,Eltham and Chislehurst,constituency,country/england,,,E14001223,constituency/Eltham and Chislehurst +constituency/E14001224,Ely and East Cambridgeshire,constituency,country/england,,,E14001224,constituency/Ely and East Cambridgeshire +constituency/E14001225,Enfield North,constituency,country/england,,,E14001225,constituency/Enfield North +constituency/E14001226,Epping Forest,constituency,country/england,,,E14001226,constituency/Epping Forest +constituency/E14001227,Epsom and Ewell,constituency,country/england,,,E14001227,constituency/Epsom and Ewell +constituency/E14001228,Erewash,constituency,country/england,,,E14001228,constituency/Erewash +constituency/E14001229,Erith and Thamesmead,constituency,country/england,,,E14001229,constituency/Erith and Thamesmead +constituency/E14001230,Esher and Walton,constituency,country/england,,,E14001230,constituency/Esher and Walton +constituency/E14001231,Exeter,constituency,country/england,,,E14001231,constituency/Exeter +constituency/E14001232,Exmouth and Exeter East,constituency,country/england,,,E14001232,constituency/Exmouth and Exeter East +constituency/E14001233,Fareham and Waterlooville,constituency,country/england,,,E14001233,constituency/Fareham and Waterlooville +constituency/E14001234,Farnham and Bordon,constituency,country/england,,,E14001234,constituency/Farnham and Bordon +constituency/E14001235,Faversham and Mid Kent,constituency,country/england,,,E14001235,constituency/Faversham and Mid Kent +constituency/E14001236,Feltham and Heston,constituency,country/england,,,E14001236,constituency/Feltham and Heston +constituency/E14001237,Filton and Bradley Stoke,constituency,country/england,,,E14001237,constituency/Filton and Bradley Stoke +constituency/E14001238,Finchley and Golders Green,constituency,country/england,,,E14001238,constituency/Finchley and Golders Green +constituency/E14001239,Folkestone and Hythe,constituency,country/england,,,E14001239,constituency/Folkestone and Hythe +constituency/E14001240,Forest of Dean,constituency,country/england,,,E14001240,constituency/Forest of Dean +constituency/E14001241,Frome and East Somerset,constituency,country/england,,,E14001241,constituency/Frome and East Somerset +constituency/E14001242,Fylde,constituency,country/england,,,E14001242,constituency/Fylde +constituency/E14001243,Gainsborough,constituency,country/england,,,E14001243,constituency/Gainsborough +constituency/E14001244,Gateshead Central and Whickham,constituency,country/england,,,E14001244,constituency/Gateshead Central and Whickham +constituency/E14001245,Gedling,constituency,country/england,,,E14001245,constituency/Gedling +constituency/E14001246,Gillingham and Rainham,constituency,country/england,,,E14001246,constituency/Gillingham and Rainham +constituency/E14001247,Glastonbury and Somerton,constituency,country/england,,,E14001247,constituency/Glastonbury and Somerton +constituency/E14001248,Gloucester,constituency,country/england,,,E14001248,constituency/Gloucester +constituency/E14001249,Godalming and Ash,constituency,country/england,,,E14001249,constituency/Godalming and Ash +constituency/E14001250,Goole and Pocklington,constituency,country/england,,,E14001250,constituency/Goole and Pocklington +constituency/E14001251,Gorton and Denton,constituency,country/england,,,E14001251,constituency/Gorton and Denton +constituency/E14001252,Gosport,constituency,country/england,,,E14001252,constituency/Gosport +constituency/E14001253,Grantham and Bourne,constituency,country/england,,,E14001253,constituency/Grantham and Bourne +constituency/E14001254,Gravesham,constituency,country/england,,,E14001254,constituency/Gravesham +constituency/E14001255,Great Grimsby and Cleethorpes,constituency,country/england,,,E14001255,constituency/Great Grimsby and Cleethorpes +constituency/E14001256,Great Yarmouth,constituency,country/england,,,E14001256,constituency/Great Yarmouth +constituency/E14001257,Greenwich and Woolwich,constituency,country/england,,,E14001257,constituency/Greenwich and Woolwich +constituency/E14001258,Guildford,constituency,country/england,,,E14001258,constituency/Guildford +constituency/E14001259,Hackney North and Stoke Newington,constituency,country/england,,,E14001259,constituency/Hackney North and Stoke Newington +constituency/E14001260,Hackney South and Shoreditch,constituency,country/england,,,E14001260,constituency/Hackney South and Shoreditch +constituency/E14001261,Halesowen,constituency,country/england,,,E14001261,constituency/Halesowen +constituency/E14001262,Halifax,constituency,country/england,,,E14001262,constituency/Halifax +constituency/E14001263,Hamble Valley,constituency,country/england,,,E14001263,constituency/Hamble Valley +constituency/E14001264,Hammersmith and Chiswick,constituency,country/england,,,E14001264,constituency/Hammersmith and Chiswick +constituency/E14001265,Hampstead and Highgate,constituency,country/england,,,E14001265,constituency/Hampstead and Highgate +constituency/E14001266,"Harborough, Oadby and Wigston",constituency,country/england,,,E14001266,"constituency/Harborough, Oadby and Wigston" +constituency/E14001267,Harlow,constituency,country/england,,,E14001267,constituency/Harlow +constituency/E14001268,Harpenden and Berkhamsted,constituency,country/england,,,E14001268,constituency/Harpenden and Berkhamsted +constituency/E14001269,Harrogate and Knaresborough,constituency,country/england,,,E14001269,constituency/Harrogate and Knaresborough +constituency/E14001270,Harrow East,constituency,country/england,,,E14001270,constituency/Harrow East +constituency/E14001271,Harrow West,constituency,country/england,,,E14001271,constituency/Harrow West +constituency/E14001272,Hartlepool,constituency,country/england,,,E14001272,constituency/Hartlepool +constituency/E14001273,Harwich and North Essex,constituency,country/england,,,E14001273,constituency/Harwich and North Essex +constituency/E14001274,Hastings and Rye,constituency,country/england,,,E14001274,constituency/Hastings and Rye +constituency/E14001275,Havant,constituency,country/england,,,E14001275,constituency/Havant +constituency/E14001276,Hayes and Harlington,constituency,country/england,,,E14001276,constituency/Hayes and Harlington +constituency/E14001277,Hazel Grove,constituency,country/england,,,E14001277,constituency/Hazel Grove +constituency/E14001278,Hemel Hempstead,constituency,country/england,,,E14001278,constituency/Hemel Hempstead +constituency/E14001279,Hendon,constituency,country/england,,,E14001279,constituency/Hendon +constituency/E14001280,Henley and Thame,constituency,country/england,,,E14001280,constituency/Henley and Thame +constituency/E14001281,Hereford and South Herefordshire,constituency,country/england,,,E14001281,constituency/Hereford and South Herefordshire +constituency/E14001282,Herne Bay and Sandwich,constituency,country/england,,,E14001282,constituency/Herne Bay and Sandwich +constituency/E14001283,Hertford and Stortford,constituency,country/england,,,E14001283,constituency/Hertford and Stortford +constituency/E14001284,Hertsmere,constituency,country/england,,,E14001284,constituency/Hertsmere +constituency/E14001285,Hexham,constituency,country/england,,,E14001285,constituency/Hexham +constituency/E14001286,Heywood and Middleton North,constituency,country/england,,,E14001286,constituency/Heywood and Middleton North +constituency/E14001287,High Peak,constituency,country/england,,,E14001287,constituency/High Peak +constituency/E14001288,Hinckley and Bosworth,constituency,country/england,,,E14001288,constituency/Hinckley and Bosworth +constituency/E14001289,Hitchin,constituency,country/england,,,E14001289,constituency/Hitchin +constituency/E14001290,Holborn and St Pancras,constituency,country/england,,,E14001290,constituency/Holborn and St Pancras +constituency/E14001291,Honiton and Sidmouth,constituency,country/england,,,E14001291,constituency/Honiton and Sidmouth +constituency/E14001292,Hornchurch and Upminster,constituency,country/england,,,E14001292,constituency/Hornchurch and Upminster +constituency/E14001293,Hornsey and Friern Barnet,constituency,country/england,,,E14001293,constituency/Hornsey and Friern Barnet +constituency/E14001294,Horsham,constituency,country/england,,,E14001294,constituency/Horsham +constituency/E14001295,Houghton and Sunderland South,constituency,country/england,,,E14001295,constituency/Houghton and Sunderland South +constituency/E14001296,Hove and Portslade,constituency,country/england,,,E14001296,constituency/Hove and Portslade +constituency/E14001297,Huddersfield,constituency,country/england,,,E14001297,constituency/Huddersfield +constituency/E14001298,Huntingdon,constituency,country/england,,,E14001298,constituency/Huntingdon +constituency/E14001299,Hyndburn,constituency,country/england,,,E14001299,constituency/Hyndburn +constituency/E14001300,Ilford North,constituency,country/england,,,E14001300,constituency/Ilford North +constituency/E14001301,Ilford South,constituency,country/england,,,E14001301,constituency/Ilford South +constituency/E14001302,Ipswich,constituency,country/england,,,E14001302,constituency/Ipswich +constituency/E14001303,Isle of Wight East,constituency,country/england,,,E14001303,constituency/Isle of Wight East +constituency/E14001304,Isle of Wight West,constituency,country/england,,,E14001304,constituency/Isle of Wight West +constituency/E14001305,Islington North,constituency,country/england,,,E14001305,constituency/Islington North +constituency/E14001306,Islington South and Finsbury,constituency,country/england,,,E14001306,constituency/Islington South and Finsbury +constituency/E14001307,Jarrow and Gateshead East,constituency,country/england,,,E14001307,constituency/Jarrow and Gateshead East +constituency/E14001308,Keighley and Ilkley,constituency,country/england,,,E14001308,constituency/Keighley and Ilkley +constituency/E14001309,Kenilworth and Southam,constituency,country/england,,,E14001309,constituency/Kenilworth and Southam +constituency/E14001310,Kensington and Bayswater,constituency,country/england,,,E14001310,constituency/Kensington and Bayswater +constituency/E14001311,Kettering,constituency,country/england,,,E14001311,constituency/Kettering +constituency/E14001312,Kingston and Surbiton,constituency,country/england,,,E14001312,constituency/Kingston and Surbiton +constituency/E14001313,Kingston upon Hull East,constituency,country/england,,,E14001313,constituency/Kingston upon Hull East +constituency/E14001314,Kingston upon Hull North and Cottingham,constituency,country/england,,,E14001314,constituency/Kingston upon Hull North and Cottingham +constituency/E14001315,Kingston upon Hull West and Haltemprice,constituency,country/england,,,E14001315,constituency/Kingston upon Hull West and Haltemprice +constituency/E14001316,Kingswinford and South Staffordshire,constituency,country/england,,,E14001316,constituency/Kingswinford and South Staffordshire +constituency/E14001317,Knowsley,constituency,country/england,,,E14001317,constituency/Knowsley +constituency/E14001318,Lancaster and Wyre,constituency,country/england,,,E14001318,constituency/Lancaster and Wyre +constituency/E14001319,Leeds Central and Headingley,constituency,country/england,,,E14001319,constituency/Leeds Central and Headingley +constituency/E14001320,Leeds East,constituency,country/england,,,E14001320,constituency/Leeds East +constituency/E14001321,Leeds North East,constituency,country/england,,,E14001321,constituency/Leeds North East +constituency/E14001322,Leeds North West,constituency,country/england,,,E14001322,constituency/Leeds North West +constituency/E14001323,Leeds South,constituency,country/england,,,E14001323,constituency/Leeds South +constituency/E14001324,Leeds South West and Morley,constituency,country/england,,,E14001324,constituency/Leeds South West and Morley +constituency/E14001325,Leeds West and Pudsey,constituency,country/england,,,E14001325,constituency/Leeds West and Pudsey +constituency/E14001326,Leicester East,constituency,country/england,,,E14001326,constituency/Leicester East +constituency/E14001327,Leicester South,constituency,country/england,,,E14001327,constituency/Leicester South +constituency/E14001328,Leicester West,constituency,country/england,,,E14001328,constituency/Leicester West +constituency/E14001329,Leigh and Atherton,constituency,country/england,,,E14001329,constituency/Leigh and Atherton +constituency/E14001330,Lewes,constituency,country/england,,,E14001330,constituency/Lewes +constituency/E14001331,Lewisham East,constituency,country/england,,,E14001331,constituency/Lewisham East +constituency/E14001332,Lewisham North,constituency,country/england,,,E14001332,constituency/Lewisham North +constituency/E14001333,Lewisham West and East Dulwich,constituency,country/england,,,E14001333,constituency/Lewisham West and East Dulwich +constituency/E14001334,Leyton and Wanstead,constituency,country/england,,,E14001334,constituency/Leyton and Wanstead +constituency/E14001335,Lichfield,constituency,country/england,,,E14001335,constituency/Lichfield +constituency/E14001336,Lincoln,constituency,country/england,,,E14001336,constituency/Lincoln +constituency/E14001337,Liverpool Garston,constituency,country/england,,,E14001337,constituency/Liverpool Garston +constituency/E14001338,Liverpool Riverside,constituency,country/england,,,E14001338,constituency/Liverpool Riverside +constituency/E14001339,Liverpool Walton,constituency,country/england,,,E14001339,constituency/Liverpool Walton +constituency/E14001340,Liverpool Wavertree,constituency,country/england,,,E14001340,constituency/Liverpool Wavertree +constituency/E14001341,Liverpool West Derby,constituency,country/england,,,E14001341,constituency/Liverpool West Derby +constituency/E14001342,Loughborough,constituency,country/england,,,E14001342,constituency/Loughborough +constituency/E14001343,Louth and Horncastle,constituency,country/england,,,E14001343,constituency/Louth and Horncastle +constituency/E14001344,Lowestoft,constituency,country/england,,,E14001344,constituency/Lowestoft +constituency/E14001345,Luton North,constituency,country/england,,,E14001345,constituency/Luton North +constituency/E14001346,Luton South and South Bedfordshire,constituency,country/england,,,E14001346,constituency/Luton South and South Bedfordshire +constituency/E14001347,Macclesfield,constituency,country/england,,,E14001347,constituency/Macclesfield +constituency/E14001348,Maidenhead,constituency,country/england,,,E14001348,constituency/Maidenhead +constituency/E14001349,Maidstone and Malling,constituency,country/england,,,E14001349,constituency/Maidstone and Malling +constituency/E14001350,Makerfield,constituency,country/england,,,E14001350,constituency/Makerfield +constituency/E14001351,Maldon,constituency,country/england,,,E14001351,constituency/Maldon +constituency/E14001352,Manchester Central,constituency,country/england,,,E14001352,constituency/Manchester Central +constituency/E14001353,Manchester Rusholme,constituency,country/england,,,E14001353,constituency/Manchester Rusholme +constituency/E14001354,Manchester Withington,constituency,country/england,,,E14001354,constituency/Manchester Withington +constituency/E14001355,Mansfield,constituency,country/england,,,E14001355,constituency/Mansfield +constituency/E14001356,Melksham and Devizes,constituency,country/england,,,E14001356,constituency/Melksham and Devizes +constituency/E14001357,Melton and Syston,constituency,country/england,,,E14001357,constituency/Melton and Syston +constituency/E14001358,Meriden and Solihull East,constituency,country/england,,,E14001358,constituency/Meriden and Solihull East +constituency/E14001359,Mid Bedfordshire,constituency,country/england,,,E14001359,constituency/Mid Bedfordshire +constituency/E14001360,Mid Buckinghamshire,constituency,country/england,,,E14001360,constituency/Mid Buckinghamshire +constituency/E14001361,Mid Cheshire,constituency,country/england,,,E14001361,constituency/Mid Cheshire +constituency/E14001362,Mid Derbyshire,constituency,country/england,,,E14001362,constituency/Mid Derbyshire +constituency/E14001363,Mid Dorset and North Poole,constituency,country/england,,,E14001363,constituency/Mid Dorset and North Poole +constituency/E14001364,Mid Leicestershire,constituency,country/england,,,E14001364,constituency/Mid Leicestershire +constituency/E14001365,Mid Norfolk,constituency,country/england,,,E14001365,constituency/Mid Norfolk +constituency/E14001366,Mid Sussex,constituency,country/england,,,E14001366,constituency/Mid Sussex +constituency/E14001367,Middlesbrough and Thornaby East,constituency,country/england,,,E14001367,constituency/Middlesbrough and Thornaby East +constituency/E14001368,Middlesbrough South and East Cleveland,constituency,country/england,,,E14001368,constituency/Middlesbrough South and East Cleveland +constituency/E14001369,Milton Keynes Central,constituency,country/england,,,E14001369,constituency/Milton Keynes Central +constituency/E14001370,Milton Keynes North,constituency,country/england,,,E14001370,constituency/Milton Keynes North +constituency/E14001371,Mitcham and Morden,constituency,country/england,,,E14001371,constituency/Mitcham and Morden +constituency/E14001372,Morecambe and Lunesdale,constituency,country/england,,,E14001372,constituency/Morecambe and Lunesdale +constituency/E14001373,New Forest East,constituency,country/england,,,E14001373,constituency/New Forest East +constituency/E14001374,New Forest West,constituency,country/england,,,E14001374,constituency/New Forest West +constituency/E14001375,Newark,constituency,country/england,,,E14001375,constituency/Newark +constituency/E14001376,Newbury,constituency,country/england,,,E14001376,constituency/Newbury +constituency/E14001377,Newcastle upon Tyne Central and West,constituency,country/england,,,E14001377,constituency/Newcastle upon Tyne Central and West +constituency/E14001378,Newcastle upon Tyne East and Wallsend,constituency,country/england,,,E14001378,constituency/Newcastle upon Tyne East and Wallsend +constituency/E14001379,Newcastle upon Tyne North,constituency,country/england,,,E14001379,constituency/Newcastle upon Tyne North +constituency/E14001380,Newcastle-under-Lyme,constituency,country/england,,,E14001380,constituency/Newcastle-under-Lyme +constituency/E14001381,Newton Abbot,constituency,country/england,,,E14001381,constituency/Newton Abbot +constituency/E14001382,Newton Aycliffe and Spennymoor,constituency,country/england,,,E14001382,constituency/Newton Aycliffe and Spennymoor +constituency/E14001383,Normanton and Hemsworth,constituency,country/england,,,E14001383,constituency/Normanton and Hemsworth +constituency/E14001384,North Bedfordshire,constituency,country/england,,,E14001384,constituency/North Bedfordshire +constituency/E14001385,North Cornwall,constituency,country/england,,,E14001385,constituency/North Cornwall +constituency/E14001386,North Cotswolds,constituency,country/england,,,E14001386,constituency/North Cotswolds +constituency/E14001387,North Devon,constituency,country/england,,,E14001387,constituency/North Devon +constituency/E14001388,North Dorset,constituency,country/england,,,E14001388,constituency/North Dorset +constituency/E14001389,North Durham,constituency,country/england,,,E14001389,constituency/North Durham +constituency/E14001390,North East Cambridgeshire,constituency,country/england,,,E14001390,constituency/North East Cambridgeshire +constituency/E14001391,North East Derbyshire,constituency,country/england,,,E14001391,constituency/North East Derbyshire +constituency/E14001392,North East Hampshire,constituency,country/england,,,E14001392,constituency/North East Hampshire +constituency/E14001393,North East Hertfordshire,constituency,country/england,,,E14001393,constituency/North East Hertfordshire +constituency/E14001394,North East Somerset and Hanham,constituency,country/england,,,E14001394,constituency/North East Somerset and Hanham +constituency/E14001395,North Herefordshire,constituency,country/england,,,E14001395,constituency/North Herefordshire +constituency/E14001396,North Norfolk,constituency,country/england,,,E14001396,constituency/North Norfolk +constituency/E14001397,North Northumberland,constituency,country/england,,,E14001397,constituency/North Northumberland +constituency/E14001398,North Shropshire,constituency,country/england,,,E14001398,constituency/North Shropshire +constituency/E14001399,North Somerset,constituency,country/england,,,E14001399,constituency/North Somerset +constituency/E14001400,North Warwickshire and Bedworth,constituency,country/england,,,E14001400,constituency/North Warwickshire and Bedworth +constituency/E14001401,North West Cambridgeshire,constituency,country/england,,,E14001401,constituency/North West Cambridgeshire +constituency/E14001402,North West Essex,constituency,country/england,,,E14001402,constituency/North West Essex +constituency/E14001403,North West Hampshire,constituency,country/england,,,E14001403,constituency/North West Hampshire +constituency/E14001404,North West Leicestershire,constituency,country/england,,,E14001404,constituency/North West Leicestershire +constituency/E14001405,North West Norfolk,constituency,country/england,,,E14001405,constituency/North West Norfolk +constituency/E14001406,Northampton North,constituency,country/england,,,E14001406,constituency/Northampton North +constituency/E14001407,Northampton South,constituency,country/england,,,E14001407,constituency/Northampton South +constituency/E14001408,Norwich North,constituency,country/england,,,E14001408,constituency/Norwich North +constituency/E14001409,Norwich South,constituency,country/england,,,E14001409,constituency/Norwich South +constituency/E14001410,Nottingham East,constituency,country/england,,,E14001410,constituency/Nottingham East +constituency/E14001411,Nottingham North and Kimberley,constituency,country/england,,,E14001411,constituency/Nottingham North and Kimberley +constituency/E14001412,Nottingham South,constituency,country/england,,,E14001412,constituency/Nottingham South +constituency/E14001413,Nuneaton,constituency,country/england,,,E14001413,constituency/Nuneaton +constituency/E14001414,Old Bexley and Sidcup,constituency,country/england,,,E14001414,constituency/Old Bexley and Sidcup +constituency/E14001415,Oldham East and Saddleworth,constituency,country/england,,,E14001415,constituency/Oldham East and Saddleworth +constituency/E14001416,"Oldham West, Chadderton and Royton",constituency,country/england,,,E14001416,"constituency/Oldham West, Chadderton and Royton" +constituency/E14001417,Orpington,constituency,country/england,,,E14001417,constituency/Orpington +constituency/E14001418,Ossett and Denby Dale,constituency,country/england,,,E14001418,constituency/Ossett and Denby Dale +constituency/E14001419,Oxford East,constituency,country/england,,,E14001419,constituency/Oxford East +constituency/E14001420,Oxford West and Abingdon,constituency,country/england,,,E14001420,constituency/Oxford West and Abingdon +constituency/E14001421,Peckham,constituency,country/england,,,E14001421,constituency/Peckham +constituency/E14001422,Pendle and Clitheroe,constituency,country/england,,,E14001422,constituency/Pendle and Clitheroe +constituency/E14001423,Penistone and Stocksbridge,constituency,country/england,,,E14001423,constituency/Penistone and Stocksbridge +constituency/E14001424,Penrith and Solway,constituency,country/england,,,E14001424,constituency/Penrith and Solway +constituency/E14001425,Peterborough,constituency,country/england,,,E14001425,constituency/Peterborough +constituency/E14001426,Plymouth Moor View,constituency,country/england,,,E14001426,constituency/Plymouth Moor View +constituency/E14001427,Plymouth Sutton and Devonport,constituency,country/england,,,E14001427,constituency/Plymouth Sutton and Devonport +constituency/E14001428,"Pontefract, Castleford and Knottingley",constituency,country/england,,,E14001428,"constituency/Pontefract, Castleford and Knottingley" +constituency/E14001429,Poole,constituency,country/england,,,E14001429,constituency/Poole +constituency/E14001430,Poplar and Limehouse,constituency,country/england,,,E14001430,constituency/Poplar and Limehouse +constituency/E14001431,Portsmouth North,constituency,country/england,,,E14001431,constituency/Portsmouth North +constituency/E14001432,Portsmouth South,constituency,country/england,,,E14001432,constituency/Portsmouth South +constituency/E14001433,Preston,constituency,country/england,,,E14001433,constituency/Preston +constituency/E14001434,Putney,constituency,country/england,,,E14001434,constituency/Putney +constituency/E14001435,Queen's Park and Maida Vale,constituency,country/england,,,E14001435,constituency/Queen's Park and Maida Vale +constituency/E14001436,Rawmarsh and Conisbrough,constituency,country/england,,,E14001436,constituency/Rawmarsh and Conisbrough +constituency/E14001437,Rayleigh and Wickford,constituency,country/england,,,E14001437,constituency/Rayleigh and Wickford +constituency/E14001438,Reading Central,constituency,country/england,,,E14001438,constituency/Reading Central +constituency/E14001439,Reading West and Mid Berkshire,constituency,country/england,,,E14001439,constituency/Reading West and Mid Berkshire +constituency/E14001440,Redcar,constituency,country/england,,,E14001440,constituency/Redcar +constituency/E14001441,Redditch,constituency,country/england,,,E14001441,constituency/Redditch +constituency/E14001442,Reigate,constituency,country/england,,,E14001442,constituency/Reigate +constituency/E14001443,Ribble Valley,constituency,country/england,,,E14001443,constituency/Ribble Valley +constituency/E14001444,Richmond and Northallerton,constituency,country/england,,,E14001444,constituency/Richmond and Northallerton +constituency/E14001445,Richmond Park,constituency,country/england,,,E14001445,constituency/Richmond Park +constituency/E14001446,Rochdale,constituency,country/england,,,E14001446,constituency/Rochdale +constituency/E14001447,Rochester and Strood,constituency,country/england,,,E14001447,constituency/Rochester and Strood +constituency/E14001448,Romford,constituency,country/england,,,E14001448,constituency/Romford +constituency/E14001449,Romsey and Southampton North,constituency,country/england,,,E14001449,constituency/Romsey and Southampton North +constituency/E14001450,Rossendale and Darwen,constituency,country/england,,,E14001450,constituency/Rossendale and Darwen +constituency/E14001451,Rother Valley,constituency,country/england,,,E14001451,constituency/Rother Valley +constituency/E14001452,Rotherham,constituency,country/england,,,E14001452,constituency/Rotherham +constituency/E14001453,Rugby,constituency,country/england,,,E14001453,constituency/Rugby +constituency/E14001454,"Ruislip, Northwood and Pinner",constituency,country/england,,,E14001454,"constituency/Ruislip, Northwood and Pinner" +constituency/E14001455,Runcorn and Helsby,constituency,country/england,,,E14001455,constituency/Runcorn and Helsby +constituency/E14001456,Runnymede and Weybridge,constituency,country/england,,,E14001456,constituency/Runnymede and Weybridge +constituency/E14001457,Rushcliffe,constituency,country/england,,,E14001457,constituency/Rushcliffe +constituency/E14001458,Rutland and Stamford,constituency,country/england,,,E14001458,constituency/Rutland and Stamford +constituency/E14001459,Salford,constituency,country/england,,,E14001459,constituency/Salford +constituency/E14001460,Salisbury,constituency,country/england,,,E14001460,constituency/Salisbury +constituency/E14001461,Scarborough and Whitby,constituency,country/england,,,E14001461,constituency/Scarborough and Whitby +constituency/E14001462,Scunthorpe,constituency,country/england,,,E14001462,constituency/Scunthorpe +constituency/E14001463,Sefton Central,constituency,country/england,,,E14001463,constituency/Sefton Central +constituency/E14001464,Selby,constituency,country/england,,,E14001464,constituency/Selby +constituency/E14001465,Sevenoaks,constituency,country/england,,,E14001465,constituency/Sevenoaks +constituency/E14001466,Sheffield Brightside and Hillsborough,constituency,country/england,,,E14001466,constituency/Sheffield Brightside and Hillsborough +constituency/E14001467,Sheffield Central,constituency,country/england,,,E14001467,constituency/Sheffield Central +constituency/E14001468,Sheffield Hallam,constituency,country/england,,,E14001468,constituency/Sheffield Hallam +constituency/E14001469,Sheffield Heeley,constituency,country/england,,,E14001469,constituency/Sheffield Heeley +constituency/E14001470,Sheffield South East,constituency,country/england,,,E14001470,constituency/Sheffield South East +constituency/E14001471,Sherwood Forest,constituency,country/england,,,E14001471,constituency/Sherwood Forest +constituency/E14001472,Shipley,constituency,country/england,,,E14001472,constituency/Shipley +constituency/E14001473,Shrewsbury,constituency,country/england,,,E14001473,constituency/Shrewsbury +constituency/E14001474,Sittingbourne and Sheppey,constituency,country/england,,,E14001474,constituency/Sittingbourne and Sheppey +constituency/E14001475,Skipton and Ripon,constituency,country/england,,,E14001475,constituency/Skipton and Ripon +constituency/E14001476,Sleaford and North Hykeham,constituency,country/england,,,E14001476,constituency/Sleaford and North Hykeham +constituency/E14001477,Slough,constituency,country/england,,,E14001477,constituency/Slough +constituency/E14001478,Smethwick,constituency,country/england,,,E14001478,constituency/Smethwick +constituency/E14001479,Solihull West and Shirley,constituency,country/england,,,E14001479,constituency/Solihull West and Shirley +constituency/E14001480,South Basildon and East Thurrock,constituency,country/england,,,E14001480,constituency/South Basildon and East Thurrock +constituency/E14001481,South Cambridgeshire,constituency,country/england,,,E14001481,constituency/South Cambridgeshire +constituency/E14001482,South Cotswolds,constituency,country/england,,,E14001482,constituency/South Cotswolds +constituency/E14001483,South Derbyshire,constituency,country/england,,,E14001483,constituency/South Derbyshire +constituency/E14001484,South Devon,constituency,country/england,,,E14001484,constituency/South Devon +constituency/E14001485,South Dorset,constituency,country/england,,,E14001485,constituency/South Dorset +constituency/E14001486,South East Cornwall,constituency,country/england,,,E14001486,constituency/South East Cornwall +constituency/E14001487,South Holland and The Deepings,constituency,country/england,,,E14001487,constituency/South Holland and The Deepings +constituency/E14001488,South Leicestershire,constituency,country/england,,,E14001488,constituency/South Leicestershire +constituency/E14001489,South Norfolk,constituency,country/england,,,E14001489,constituency/South Norfolk +constituency/E14001490,South Northamptonshire,constituency,country/england,,,E14001490,constituency/South Northamptonshire +constituency/E14001491,South Ribble,constituency,country/england,,,E14001491,constituency/South Ribble +constituency/E14001492,South Shields,constituency,country/england,,,E14001492,constituency/South Shields +constituency/E14001493,South Shropshire,constituency,country/england,,,E14001493,constituency/South Shropshire +constituency/E14001494,South Suffolk,constituency,country/england,,,E14001494,constituency/South Suffolk +constituency/E14001495,South West Devon,constituency,country/england,,,E14001495,constituency/South West Devon +constituency/E14001496,South West Hertfordshire,constituency,country/england,,,E14001496,constituency/South West Hertfordshire +constituency/E14001497,South West Norfolk,constituency,country/england,,,E14001497,constituency/South West Norfolk +constituency/E14001498,South West Wiltshire,constituency,country/england,,,E14001498,constituency/South West Wiltshire +constituency/E14001499,Southampton Itchen,constituency,country/england,,,E14001499,constituency/Southampton Itchen +constituency/E14001500,Southampton Test,constituency,country/england,,,E14001500,constituency/Southampton Test +constituency/E14001501,Southend East and Rochford,constituency,country/england,,,E14001501,constituency/Southend East and Rochford +constituency/E14001502,Southend West and Leigh,constituency,country/england,,,E14001502,constituency/Southend West and Leigh +constituency/E14001503,Southgate and Wood Green,constituency,country/england,,,E14001503,constituency/Southgate and Wood Green +constituency/E14001504,Southport,constituency,country/england,,,E14001504,constituency/Southport +constituency/E14001505,Spelthorne,constituency,country/england,,,E14001505,constituency/Spelthorne +constituency/E14001506,Spen Valley,constituency,country/england,,,E14001506,constituency/Spen Valley +constituency/E14001507,St Albans,constituency,country/england,,,E14001507,constituency/St Albans +constituency/E14001508,St Austell and Newquay,constituency,country/england,,,E14001508,constituency/St Austell and Newquay +constituency/E14001509,St Helens North,constituency,country/england,,,E14001509,constituency/St Helens North +constituency/E14001510,St Helens South and Whiston,constituency,country/england,,,E14001510,constituency/St Helens South and Whiston +constituency/E14001511,St Ives,constituency,country/england,,,E14001511,constituency/St Ives +constituency/E14001512,St Neots and Mid Cambridgeshire,constituency,country/england,,,E14001512,constituency/St Neots and Mid Cambridgeshire +constituency/E14001513,Stafford,constituency,country/england,,,E14001513,constituency/Stafford +constituency/E14001514,Staffordshire Moorlands,constituency,country/england,,,E14001514,constituency/Staffordshire Moorlands +constituency/E14001515,Stalybridge and Hyde,constituency,country/england,,,E14001515,constituency/Stalybridge and Hyde +constituency/E14001516,Stevenage,constituency,country/england,,,E14001516,constituency/Stevenage +constituency/E14001517,Stockport,constituency,country/england,,,E14001517,constituency/Stockport +constituency/E14001518,Stockton North,constituency,country/england,,,E14001518,constituency/Stockton North +constituency/E14001519,Stockton West,constituency,country/england,,,E14001519,constituency/Stockton West +constituency/E14001520,Stoke-on-Trent Central,constituency,country/england,,,E14001520,constituency/Stoke-on-Trent Central +constituency/E14001521,Stoke-on-Trent North,constituency,country/england,,,E14001521,constituency/Stoke-on-Trent North +constituency/E14001522,Stoke-on-Trent South,constituency,country/england,,,E14001522,constituency/Stoke-on-Trent South +constituency/E14001523,"Stone, Great Wyrley and Penkridge",constituency,country/england,,,E14001523,"constituency/Stone, Great Wyrley and Penkridge" +constituency/E14001524,Stourbridge,constituency,country/england,,,E14001524,constituency/Stourbridge +constituency/E14001525,Stratford and Bow,constituency,country/england,,,E14001525,constituency/Stratford and Bow +constituency/E14001526,Stratford-on-Avon,constituency,country/england,,,E14001526,constituency/Stratford-on-Avon +constituency/E14001527,Streatham and Croydon North,constituency,country/england,,,E14001527,constituency/Streatham and Croydon North +constituency/E14001528,Stretford and Urmston,constituency,country/england,,,E14001528,constituency/Stretford and Urmston +constituency/E14001529,Stroud,constituency,country/england,,,E14001529,constituency/Stroud +constituency/E14001530,Suffolk Coastal,constituency,country/england,,,E14001530,constituency/Suffolk Coastal +constituency/E14001531,Sunderland Central,constituency,country/england,,,E14001531,constituency/Sunderland Central +constituency/E14001532,Surrey Heath,constituency,country/england,,,E14001532,constituency/Surrey Heath +constituency/E14001533,Sussex Weald,constituency,country/england,,,E14001533,constituency/Sussex Weald +constituency/E14001534,Sutton and Cheam,constituency,country/england,,,E14001534,constituency/Sutton and Cheam +constituency/E14001535,Sutton Coldfield,constituency,country/england,,,E14001535,constituency/Sutton Coldfield +constituency/E14001536,Swindon North,constituency,country/england,,,E14001536,constituency/Swindon North +constituency/E14001537,Swindon South,constituency,country/england,,,E14001537,constituency/Swindon South +constituency/E14001538,Tamworth,constituency,country/england,,,E14001538,constituency/Tamworth +constituency/E14001539,Tatton,constituency,country/england,,,E14001539,constituency/Tatton +constituency/E14001540,Taunton and Wellington,constituency,country/england,,,E14001540,constituency/Taunton and Wellington +constituency/E14001541,Telford,constituency,country/england,,,E14001541,constituency/Telford +constituency/E14001542,Tewkesbury,constituency,country/england,,,E14001542,constituency/Tewkesbury +constituency/E14001543,The Wrekin,constituency,country/england,,,E14001543,constituency/The Wrekin +constituency/E14001544,Thirsk and Malton,constituency,country/england,,,E14001544,constituency/Thirsk and Malton +constituency/E14001545,Thornbury and Yate,constituency,country/england,,,E14001545,constituency/Thornbury and Yate +constituency/E14001546,Thurrock,constituency,country/england,,,E14001546,constituency/Thurrock +constituency/E14001547,Tipton and Wednesbury,constituency,country/england,,,E14001547,constituency/Tipton and Wednesbury +constituency/E14001548,Tiverton and Minehead,constituency,country/england,,,E14001548,constituency/Tiverton and Minehead +constituency/E14001549,Tonbridge,constituency,country/england,,,E14001549,constituency/Tonbridge +constituency/E14001550,Tooting,constituency,country/england,,,E14001550,constituency/Tooting +constituency/E14001551,Torbay,constituency,country/england,,,E14001551,constituency/Torbay +constituency/E14001552,Torridge and Tavistock,constituency,country/england,,,E14001552,constituency/Torridge and Tavistock +constituency/E14001553,Tottenham,constituency,country/england,,,E14001553,constituency/Tottenham +constituency/E14001554,Truro and Falmouth,constituency,country/england,,,E14001554,constituency/Truro and Falmouth +constituency/E14001555,Tunbridge Wells,constituency,country/england,,,E14001555,constituency/Tunbridge Wells +constituency/E14001556,Twickenham,constituency,country/england,,,E14001556,constituency/Twickenham +constituency/E14001557,Tynemouth,constituency,country/england,,,E14001557,constituency/Tynemouth +constituency/E14001558,Uxbridge and South Ruislip,constituency,country/england,,,E14001558,constituency/Uxbridge and South Ruislip +constituency/E14001559,Vauxhall and Camberwell Green,constituency,country/england,,,E14001559,constituency/Vauxhall and Camberwell Green +constituency/E14001560,Wakefield and Rothwell,constituency,country/england,,,E14001560,constituency/Wakefield and Rothwell +constituency/E14001561,Wallasey,constituency,country/england,,,E14001561,constituency/Wallasey +constituency/E14001562,Walsall and Bloxwich,constituency,country/england,,,E14001562,constituency/Walsall and Bloxwich +constituency/E14001563,Walthamstow,constituency,country/england,,,E14001563,constituency/Walthamstow +constituency/E14001564,Warrington North,constituency,country/england,,,E14001564,constituency/Warrington North +constituency/E14001565,Warrington South,constituency,country/england,,,E14001565,constituency/Warrington South +constituency/E14001566,Warwick and Leamington,constituency,country/england,,,E14001566,constituency/Warwick and Leamington +constituency/E14001567,Washington and Gateshead South,constituency,country/england,,,E14001567,constituency/Washington and Gateshead South +constituency/E14001568,Watford,constituency,country/england,,,E14001568,constituency/Watford +constituency/E14001569,Waveney Valley,constituency,country/england,,,E14001569,constituency/Waveney Valley +constituency/E14001570,Weald of Kent,constituency,country/england,,,E14001570,constituency/Weald of Kent +constituency/E14001571,Wellingborough and Rushden,constituency,country/england,,,E14001571,constituency/Wellingborough and Rushden +constituency/E14001572,Wells and Mendip Hills,constituency,country/england,,,E14001572,constituency/Wells and Mendip Hills +constituency/E14001573,Welwyn Hatfield,constituency,country/england,,,E14001573,constituency/Welwyn Hatfield +constituency/E14001574,West Bromwich,constituency,country/england,,,E14001574,constituency/West Bromwich +constituency/E14001575,West Dorset,constituency,country/england,,,E14001575,constituency/West Dorset +constituency/E14001576,West Ham and Beckton,constituency,country/england,,,E14001576,constituency/West Ham and Beckton +constituency/E14001577,West Lancashire,constituency,country/england,,,E14001577,constituency/West Lancashire +constituency/E14001578,West Suffolk,constituency,country/england,,,E14001578,constituency/West Suffolk +constituency/E14001579,West Worcestershire,constituency,country/england,,,E14001579,constituency/West Worcestershire +constituency/E14001580,Westmorland and Lonsdale,constituency,country/england,,,E14001580,constituency/Westmorland and Lonsdale +constituency/E14001581,Weston-super-Mare,constituency,country/england,,,E14001581,constituency/Weston-super-Mare +constituency/E14001582,Wetherby and Easingwold,constituency,country/england,,,E14001582,constituency/Wetherby and Easingwold +constituency/E14001583,Whitehaven and Workington,constituency,country/england,,,E14001583,constituency/Whitehaven and Workington +constituency/E14001584,Widnes and Halewood,constituency,country/england,,,E14001584,constituency/Widnes and Halewood +constituency/E14001585,Wigan,constituency,country/england,,,E14001585,constituency/Wigan +constituency/E14001586,Wimbledon,constituency,country/england,,,E14001586,constituency/Wimbledon +constituency/E14001587,Winchester,constituency,country/england,,,E14001587,constituency/Winchester +constituency/E14001588,Windsor,constituency,country/england,,,E14001588,constituency/Windsor +constituency/E14001589,Wirral West,constituency,country/england,,,E14001589,constituency/Wirral West +constituency/E14001590,Witham,constituency,country/england,,,E14001590,constituency/Witham +constituency/E14001591,Witney,constituency,country/england,,,E14001591,constituency/Witney +constituency/E14001592,Woking,constituency,country/england,,,E14001592,constituency/Woking +constituency/E14001593,Wokingham,constituency,country/england,,,E14001593,constituency/Wokingham +constituency/E14001594,Wolverhampton North East,constituency,country/england,,,E14001594,constituency/Wolverhampton North East +constituency/E14001595,Wolverhampton South East,constituency,country/england,,,E14001595,constituency/Wolverhampton South East +constituency/E14001596,Wolverhampton West,constituency,country/england,,,E14001596,constituency/Wolverhampton West +constituency/E14001597,Worcester,constituency,country/england,,,E14001597,constituency/Worcester +constituency/E14001598,Worsley and Eccles,constituency,country/england,,,E14001598,constituency/Worsley and Eccles +constituency/E14001599,Worthing West,constituency,country/england,,,E14001599,constituency/Worthing West +constituency/E14001600,Wycombe,constituency,country/england,,,E14001600,constituency/Wycombe +constituency/E14001601,Wyre Forest,constituency,country/england,,,E14001601,constituency/Wyre Forest +constituency/E14001602,Wythenshawe and Sale East,constituency,country/england,,,E14001602,constituency/Wythenshawe and Sale East +constituency/E14001603,Yeovil,constituency,country/england,,,E14001603,constituency/Yeovil +constituency/E14001604,York Central,constituency,country/england,,,E14001604,constituency/York Central +constituency/E14001605,York Outer,constituency,country/england,,,E14001605,constituency/York Outer +constituency/N05000001,Belfast East,constituency,country/northern_ireland,,,N05000001,constituency/Belfast East +constituency/N05000002,Belfast North,constituency,country/northern_ireland,,,N05000002,constituency/Belfast North +constituency/N05000003,Belfast South and Mid Down,constituency,country/northern_ireland,,,N05000003,constituency/Belfast South and Mid Down +constituency/N05000004,Belfast West,constituency,country/northern_ireland,,,N05000004,constituency/Belfast West +constituency/N05000005,East Antrim,constituency,country/northern_ireland,,,N05000005,constituency/East Antrim +constituency/N05000006,East Londonderry,constituency,country/northern_ireland,,,N05000006,constituency/East Londonderry +constituency/N05000007,Fermanagh and South Tyrone,constituency,country/northern_ireland,,,N05000007,constituency/Fermanagh and South Tyrone +constituency/N05000008,Foyle,constituency,country/northern_ireland,,,N05000008,constituency/Foyle +constituency/N05000009,Lagan Valley,constituency,country/northern_ireland,,,N05000009,constituency/Lagan Valley +constituency/N05000010,Mid Ulster,constituency,country/northern_ireland,,,N05000010,constituency/Mid Ulster +constituency/N05000011,Newry and Armagh,constituency,country/northern_ireland,,,N05000011,constituency/Newry and Armagh +constituency/N05000012,North Antrim,constituency,country/northern_ireland,,,N05000012,constituency/North Antrim +constituency/N05000013,North Down,constituency,country/northern_ireland,,,N05000013,constituency/North Down +constituency/N05000014,South Antrim,constituency,country/northern_ireland,,,N05000014,constituency/South Antrim +constituency/N05000015,South Down,constituency,country/northern_ireland,,,N05000015,constituency/South Down +constituency/N05000016,Strangford,constituency,country/northern_ireland,,,N05000016,constituency/Strangford +constituency/N05000017,Upper Bann,constituency,country/northern_ireland,,,N05000017,constituency/Upper Bann +constituency/N05000018,West Tyrone,constituency,country/northern_ireland,,,N05000018,constituency/West Tyrone +constituency/S14000021,East Renfrewshire,constituency,country/scotland,,,S14000021,constituency/East Renfrewshire +constituency/S14000027,Na h-Eileanan an Iar,constituency,country/scotland,,,S14000027,constituency/Na h-Eileanan an Iar +constituency/S14000045,Midlothian,constituency,country/scotland,,,S14000045,constituency/Midlothian +constituency/S14000048,North Ayrshire and Arran,constituency,country/scotland,,,S14000048,constituency/North Ayrshire and Arran +constituency/S14000051,Orkney and Shetland,constituency,country/scotland,,,S14000051,constituency/Orkney and Shetland +constituency/S14000060,Aberdeen North,constituency,country/scotland,,,S14000060,constituency/Aberdeen North +constituency/S14000061,Aberdeen South,constituency,country/scotland,,,S14000061,constituency/Aberdeen South +constituency/S14000062,Aberdeenshire North and Moray East,constituency,country/scotland,,,S14000062,constituency/Aberdeenshire North and Moray East +constituency/S14000063,Airdrie and Shotts,constituency,country/scotland,,,S14000063,constituency/Airdrie and Shotts +constituency/S14000064,Alloa and Grangemouth,constituency,country/scotland,,,S14000064,constituency/Alloa and Grangemouth +constituency/S14000065,Angus and Perthshire Glens,constituency,country/scotland,,,S14000065,constituency/Angus and Perthshire Glens +constituency/S14000066,Arbroath and Broughty Ferry,constituency,country/scotland,,,S14000066,constituency/Arbroath and Broughty Ferry +constituency/S14000067,"Argyll, Bute and South Lochaber",constituency,country/scotland,,,S14000067,"constituency/Argyll, Bute and South Lochaber" +constituency/S14000068,Bathgate and Linlithgow,constituency,country/scotland,,,S14000068,constituency/Bathgate and Linlithgow +constituency/S14000069,"Caithness, Sutherland and Easter Ross",constituency,country/scotland,,,S14000069,"constituency/Caithness, Sutherland and Easter Ross" +constituency/S14000070,Coatbridge and Bellshill,constituency,country/scotland,,,S14000070,constituency/Coatbridge and Bellshill +constituency/S14000071,Cowdenbeath and Kirkcaldy,constituency,country/scotland,,,S14000071,constituency/Cowdenbeath and Kirkcaldy +constituency/S14000072,Cumbernauld and Kirkintilloch,constituency,country/scotland,,,S14000072,constituency/Cumbernauld and Kirkintilloch +constituency/S14000073,Dumfries and Galloway,constituency,country/scotland,,,S14000073,constituency/Dumfries and Galloway +constituency/S14000074,"Dumfriesshire, Clydesdale and Tweeddale",constituency,country/scotland,,,S14000074,"constituency/Dumfriesshire, Clydesdale and Tweeddale" +constituency/S14000075,Dundee Central,constituency,country/scotland,,,S14000075,constituency/Dundee Central +constituency/S14000076,Dunfermline and Dollar,constituency,country/scotland,,,S14000076,constituency/Dunfermline and Dollar +constituency/S14000077,East Kilbride and Strathaven,constituency,country/scotland,,,S14000077,constituency/East Kilbride and Strathaven +constituency/S14000078,Edinburgh East and Musselburgh,constituency,country/scotland,,,S14000078,constituency/Edinburgh East and Musselburgh +constituency/S14000079,Edinburgh North and Leith,constituency,country/scotland,,,S14000079,constituency/Edinburgh North and Leith +constituency/S14000080,Edinburgh South,constituency,country/scotland,,,S14000080,constituency/Edinburgh South +constituency/S14000081,Edinburgh South West,constituency,country/scotland,,,S14000081,constituency/Edinburgh South West +constituency/S14000082,Edinburgh West,constituency,country/scotland,,,S14000082,constituency/Edinburgh West +constituency/S14000083,Falkirk,constituency,country/scotland,,,S14000083,constituency/Falkirk +constituency/S14000084,Glasgow East,constituency,country/scotland,,,S14000084,constituency/Glasgow East +constituency/S14000085,Glasgow North,constituency,country/scotland,,,S14000085,constituency/Glasgow North +constituency/S14000086,Glasgow North East,constituency,country/scotland,,,S14000086,constituency/Glasgow North East +constituency/S14000087,Glasgow South,constituency,country/scotland,,,S14000087,constituency/Glasgow South +constituency/S14000088,Glasgow South West,constituency,country/scotland,,,S14000088,constituency/Glasgow South West +constituency/S14000089,Glasgow West,constituency,country/scotland,,,S14000089,constituency/Glasgow West +constituency/S14000090,Glenrothes and Mid Fife,constituency,country/scotland,,,S14000090,constituency/Glenrothes and Mid Fife +constituency/S14000091,Gordon and Buchan,constituency,country/scotland,,,S14000091,constituency/Gordon and Buchan +constituency/S14000092,Hamilton and Clyde Valley,constituency,country/scotland,,,S14000092,constituency/Hamilton and Clyde Valley +constituency/S14000093,Inverclyde and Renfrewshire West,constituency,country/scotland,,,S14000093,constituency/Inverclyde and Renfrewshire West +constituency/S14000094,"Inverness, Skye and West Ross-shire",constituency,country/scotland,,,S14000094,"constituency/Inverness, Skye and West Ross-shire" +constituency/S14000095,Livingston,constituency,country/scotland,,,S14000095,constituency/Livingston +constituency/S14000096,Lothian East,constituency,country/scotland,,,S14000096,constituency/Lothian East +constituency/S14000097,Mid Dunbartonshire,constituency,country/scotland,,,S14000097,constituency/Mid Dunbartonshire +constituency/S14000098,"Moray West, Nairn and Strathspey",constituency,country/scotland,,,S14000098,"constituency/Moray West, Nairn and Strathspey" +constituency/S14000099,"Motherwell, Wishaw and Carluke",constituency,country/scotland,,,S14000099,"constituency/Motherwell, Wishaw and Carluke" +constituency/S14000100,North East Fife,constituency,country/scotland,,,S14000100,constituency/North East Fife +constituency/S14000101,Paisley and Renfrewshire North,constituency,country/scotland,,,S14000101,constituency/Paisley and Renfrewshire North +constituency/S14000102,Paisley and Renfrewshire South,constituency,country/scotland,,,S14000102,constituency/Paisley and Renfrewshire South +constituency/S14000103,Perth and Kinross-shire,constituency,country/scotland,,,S14000103,constituency/Perth and Kinross-shire +constituency/S14000104,Rutherglen,constituency,country/scotland,,,S14000104,constituency/Rutherglen +constituency/S14000105,Stirling and Strathallan,constituency,country/scotland,,,S14000105,constituency/Stirling and Strathallan +constituency/S14000106,West Dunbartonshire,constituency,country/scotland,,,S14000106,constituency/West Dunbartonshire +constituency/S14000107,"Ayr, Carrick and Cumnock",constituency,country/scotland,,,S14000107,"constituency/Ayr, Carrick and Cumnock" +constituency/S14000108,"Berwickshire, Roxburgh and Selkirk",constituency,country/scotland,,,S14000108,"constituency/Berwickshire, Roxburgh and Selkirk" +constituency/S14000109,Central Ayrshire,constituency,country/scotland,,,S14000109,constituency/Central Ayrshire +constituency/S14000110,Kilmarnock and Loudoun,constituency,country/scotland,,,S14000110,constituency/Kilmarnock and Loudoun +constituency/S14000111,West Aberdeenshire and Kincardine,constituency,country/scotland,,,S14000111,constituency/West Aberdeenshire and Kincardine +constituency/W07000081,Aberafan Maesteg,constituency,country/wales,,,W07000081,constituency/Aberafan Maesteg +constituency/W07000082,Alyn and Deeside,constituency,country/wales,,,W07000082,constituency/Alyn and Deeside +constituency/W07000083,Bangor Aberconwy,constituency,country/wales,,,W07000083,constituency/Bangor Aberconwy +constituency/W07000084,Blaenau Gwent and Rhymney,constituency,country/wales,,,W07000084,constituency/Blaenau Gwent and Rhymney +constituency/W07000085,"Brecon, Radnor and Cwm Tawe",constituency,country/wales,,,W07000085,"constituency/Brecon, Radnor and Cwm Tawe" +constituency/W07000086,Bridgend,constituency,country/wales,,,W07000086,constituency/Bridgend +constituency/W07000087,Caerfyrddin,constituency,country/wales,,,W07000087,constituency/Caerfyrddin +constituency/W07000088,Caerphilly,constituency,country/wales,,,W07000088,constituency/Caerphilly +constituency/W07000089,Cardiff East,constituency,country/wales,,,W07000089,constituency/Cardiff East +constituency/W07000090,Cardiff North,constituency,country/wales,,,W07000090,constituency/Cardiff North +constituency/W07000091,Cardiff South and Penarth,constituency,country/wales,,,W07000091,constituency/Cardiff South and Penarth +constituency/W07000092,Cardiff West,constituency,country/wales,,,W07000092,constituency/Cardiff West +constituency/W07000093,Ceredigion Preseli,constituency,country/wales,,,W07000093,constituency/Ceredigion Preseli +constituency/W07000094,Clwyd East,constituency,country/wales,,,W07000094,constituency/Clwyd East +constituency/W07000095,Clwyd North,constituency,country/wales,,,W07000095,constituency/Clwyd North +constituency/W07000096,Dwyfor Meirionnydd,constituency,country/wales,,,W07000096,constituency/Dwyfor Meirionnydd +constituency/W07000097,Gower,constituency,country/wales,,,W07000097,constituency/Gower +constituency/W07000098,Llanelli,constituency,country/wales,,,W07000098,constituency/Llanelli +constituency/W07000099,Merthyr Tydfil and Aberdare,constituency,country/wales,,,W07000099,constituency/Merthyr Tydfil and Aberdare +constituency/W07000100,Mid and South Pembrokeshire,constituency,country/wales,,,W07000100,constituency/Mid and South Pembrokeshire +constituency/W07000101,Monmouthshire,constituency,country/wales,,,W07000101,constituency/Monmouthshire +constituency/W07000102,Montgomeryshire and Glyndwr,constituency,country/wales,,,W07000102,constituency/Montgomeryshire and Glyndwr +constituency/W07000103,Neath and Swansea East,constituency,country/wales,,,W07000103,constituency/Neath and Swansea East +constituency/W07000104,Newport East,constituency,country/wales,,,W07000104,constituency/Newport East +constituency/W07000105,Newport West and Islwyn,constituency,country/wales,,,W07000105,constituency/Newport West and Islwyn +constituency/W07000106,Pontypridd,constituency,country/wales,,,W07000106,constituency/Pontypridd +constituency/W07000107,Rhondda and Ogmore,constituency,country/wales,,,W07000107,constituency/Rhondda and Ogmore +constituency/W07000108,Swansea West,constituency,country/wales,,,W07000108,constituency/Swansea West +constituency/W07000109,Torfaen,constituency,country/wales,,,W07000109,constituency/Torfaen +constituency/W07000110,Vale of Glamorgan,constituency,country/wales,,,W07000110,constituency/Vale of Glamorgan +constituency/W07000111,Wrexham,constituency,country/wales,,,W07000111,constituency/Wrexham +constituency/W07000112,Ynys Môn,constituency,country/wales,,,W07000112,constituency/Ynys Môn +local_authority/E06000001,Hartlepool,local_authority,country/england,,,E06000001,local_authority/Hartlepool +local_authority/E06000002,Middlesbrough,local_authority,country/england,,,E06000002,local_authority/Middlesbrough +local_authority/E06000003,Redcar and Cleveland,local_authority,country/england,,,E06000003,local_authority/Redcar and Cleveland +local_authority/E06000004,Stockton-on-Tees,local_authority,country/england,,,E06000004,local_authority/Stockton-on-Tees +local_authority/E06000005,Darlington,local_authority,country/england,,,E06000005,local_authority/Darlington +local_authority/E06000006,Halton,local_authority,country/england,,,E06000006,local_authority/Halton +local_authority/E06000007,Warrington,local_authority,country/england,,,E06000007,local_authority/Warrington +local_authority/E06000008,Blackburn with Darwen,local_authority,country/england,,,E06000008,local_authority/Blackburn with Darwen +local_authority/E06000009,Blackpool,local_authority,country/england,,,E06000009,local_authority/Blackpool +local_authority/E06000010,"Kingston upon Hull, City of",local_authority,country/england,,,E06000010,"local_authority/Kingston upon Hull, City of" +local_authority/E06000011,East Riding of Yorkshire,local_authority,country/england,,,E06000011,local_authority/East Riding of Yorkshire +local_authority/E06000012,North East Lincolnshire,local_authority,country/england,,,E06000012,local_authority/North East Lincolnshire +local_authority/E06000013,North Lincolnshire,local_authority,country/england,,,E06000013,local_authority/North Lincolnshire +local_authority/E06000014,York,local_authority,country/england,,,E06000014,local_authority/York +local_authority/E06000015,Derby,local_authority,country/england,,,E06000015,local_authority/Derby +local_authority/E06000016,Leicester,local_authority,country/england,,,E06000016,local_authority/Leicester +local_authority/E06000017,Rutland,local_authority,country/england,,,E06000017,local_authority/Rutland +local_authority/E06000018,Nottingham,local_authority,country/england,,,E06000018,local_authority/Nottingham +local_authority/E06000019,"Herefordshire, County of",local_authority,country/england,,,E06000019,"local_authority/Herefordshire, County of" +local_authority/E06000020,Telford and Wrekin,local_authority,country/england,,,E06000020,local_authority/Telford and Wrekin +local_authority/E06000021,Stoke-on-Trent,local_authority,country/england,,,E06000021,local_authority/Stoke-on-Trent +local_authority/E06000022,Bath and North East Somerset,local_authority,country/england,,,E06000022,local_authority/Bath and North East Somerset +local_authority/E06000023,"Bristol, City of",local_authority,country/england,,,E06000023,"local_authority/Bristol, City of" +local_authority/E06000024,North Somerset,local_authority,country/england,,,E06000024,local_authority/North Somerset +local_authority/E06000025,South Gloucestershire,local_authority,country/england,,,E06000025,local_authority/South Gloucestershire +local_authority/E06000026,Plymouth,local_authority,country/england,,,E06000026,local_authority/Plymouth +local_authority/E06000027,Torbay,local_authority,country/england,,,E06000027,local_authority/Torbay +local_authority/E06000030,Swindon,local_authority,country/england,,,E06000030,local_authority/Swindon +local_authority/E06000031,Peterborough,local_authority,country/england,,,E06000031,local_authority/Peterborough +local_authority/E06000032,Luton,local_authority,country/england,,,E06000032,local_authority/Luton +local_authority/E06000033,Southend-on-Sea,local_authority,country/england,,,E06000033,local_authority/Southend-on-Sea +local_authority/E06000034,Thurrock,local_authority,country/england,,,E06000034,local_authority/Thurrock +local_authority/E06000035,Medway,local_authority,country/england,,,E06000035,local_authority/Medway +local_authority/E06000036,Bracknell Forest,local_authority,country/england,,,E06000036,local_authority/Bracknell Forest +local_authority/E06000037,West Berkshire,local_authority,country/england,,,E06000037,local_authority/West Berkshire +local_authority/E06000038,Reading,local_authority,country/england,,,E06000038,local_authority/Reading +local_authority/E06000039,Slough,local_authority,country/england,,,E06000039,local_authority/Slough +local_authority/E06000040,Windsor and Maidenhead,local_authority,country/england,,,E06000040,local_authority/Windsor and Maidenhead +local_authority/E06000041,Wokingham,local_authority,country/england,,,E06000041,local_authority/Wokingham +local_authority/E06000042,Milton Keynes,local_authority,country/england,,,E06000042,local_authority/Milton Keynes +local_authority/E06000043,Brighton and Hove,local_authority,country/england,,,E06000043,local_authority/Brighton and Hove +local_authority/E06000044,Portsmouth,local_authority,country/england,,,E06000044,local_authority/Portsmouth +local_authority/E06000045,Southampton,local_authority,country/england,,,E06000045,local_authority/Southampton +local_authority/E06000046,Isle of Wight,local_authority,country/england,,,E06000046,local_authority/Isle of Wight +local_authority/E06000047,County Durham,local_authority,country/england,,,E06000047,local_authority/County Durham +local_authority/E06000049,Cheshire East,local_authority,country/england,,,E06000049,local_authority/Cheshire East +local_authority/E06000050,Cheshire West and Chester,local_authority,country/england,,,E06000050,local_authority/Cheshire West and Chester +local_authority/E06000051,Shropshire,local_authority,country/england,,,E06000051,local_authority/Shropshire +local_authority/E06000052,Cornwall,local_authority,country/england,,,E06000052,local_authority/Cornwall +local_authority/E06000053,Isles of Scilly,local_authority,country/england,,,E06000053,local_authority/Isles of Scilly +local_authority/E06000054,Wiltshire,local_authority,country/england,,,E06000054,local_authority/Wiltshire +local_authority/E06000055,Bedford,local_authority,country/england,,,E06000055,local_authority/Bedford +local_authority/E06000056,Central Bedfordshire,local_authority,country/england,,,E06000056,local_authority/Central Bedfordshire +local_authority/E06000057,Northumberland,local_authority,country/england,,,E06000057,local_authority/Northumberland +local_authority/E06000058,"Bournemouth, Christchurch and Poole",local_authority,country/england,,,E06000058,"local_authority/Bournemouth, Christchurch and Poole" +local_authority/E06000059,Dorset,local_authority,country/england,,,E06000059,local_authority/Dorset +local_authority/E06000060,Buckinghamshire,local_authority,country/england,,,E06000060,local_authority/Buckinghamshire +local_authority/E06000061,North Northamptonshire,local_authority,country/england,,,E06000061,local_authority/North Northamptonshire +local_authority/E06000062,West Northamptonshire,local_authority,country/england,,,E06000062,local_authority/West Northamptonshire +local_authority/E06000063,Cumberland,local_authority,country/england,,,E06000063,local_authority/Cumberland +local_authority/E06000064,Westmorland and Furness,local_authority,country/england,,,E06000064,local_authority/Westmorland and Furness +local_authority/E06000065,North Yorkshire,local_authority,country/england,,,E06000065,local_authority/North Yorkshire +local_authority/E06000066,Somerset,local_authority,country/england,,,E06000066,local_authority/Somerset +local_authority/E07000008,Cambridge,local_authority,country/england,,,E07000008,local_authority/Cambridge +local_authority/E07000009,East Cambridgeshire,local_authority,country/england,,,E07000009,local_authority/East Cambridgeshire +local_authority/E07000010,Fenland,local_authority,country/england,,,E07000010,local_authority/Fenland +local_authority/E07000011,Huntingdonshire,local_authority,country/england,,,E07000011,local_authority/Huntingdonshire +local_authority/E07000012,South Cambridgeshire,local_authority,country/england,,,E07000012,local_authority/South Cambridgeshire +local_authority/E07000032,Amber Valley,local_authority,country/england,,,E07000032,local_authority/Amber Valley +local_authority/E07000033,Bolsover,local_authority,country/england,,,E07000033,local_authority/Bolsover +local_authority/E07000034,Chesterfield,local_authority,country/england,,,E07000034,local_authority/Chesterfield +local_authority/E07000035,Derbyshire Dales,local_authority,country/england,,,E07000035,local_authority/Derbyshire Dales +local_authority/E07000036,Erewash,local_authority,country/england,,,E07000036,local_authority/Erewash +local_authority/E07000037,High Peak,local_authority,country/england,,,E07000037,local_authority/High Peak +local_authority/E07000038,North East Derbyshire,local_authority,country/england,,,E07000038,local_authority/North East Derbyshire +local_authority/E07000039,South Derbyshire,local_authority,country/england,,,E07000039,local_authority/South Derbyshire +local_authority/E07000040,East Devon,local_authority,country/england,,,E07000040,local_authority/East Devon +local_authority/E07000041,Exeter,local_authority,country/england,,,E07000041,local_authority/Exeter +local_authority/E07000042,Mid Devon,local_authority,country/england,,,E07000042,local_authority/Mid Devon +local_authority/E07000043,North Devon,local_authority,country/england,,,E07000043,local_authority/North Devon +local_authority/E07000044,South Hams,local_authority,country/england,,,E07000044,local_authority/South Hams +local_authority/E07000045,Teignbridge,local_authority,country/england,,,E07000045,local_authority/Teignbridge +local_authority/E07000046,Torridge,local_authority,country/england,,,E07000046,local_authority/Torridge +local_authority/E07000047,West Devon,local_authority,country/england,,,E07000047,local_authority/West Devon +local_authority/E07000061,Eastbourne,local_authority,country/england,,,E07000061,local_authority/Eastbourne +local_authority/E07000062,Hastings,local_authority,country/england,,,E07000062,local_authority/Hastings +local_authority/E07000063,Lewes,local_authority,country/england,,,E07000063,local_authority/Lewes +local_authority/E07000064,Rother,local_authority,country/england,,,E07000064,local_authority/Rother +local_authority/E07000065,Wealden,local_authority,country/england,,,E07000065,local_authority/Wealden +local_authority/E07000066,Basildon,local_authority,country/england,,,E07000066,local_authority/Basildon +local_authority/E07000067,Braintree,local_authority,country/england,,,E07000067,local_authority/Braintree +local_authority/E07000068,Brentwood,local_authority,country/england,,,E07000068,local_authority/Brentwood +local_authority/E07000069,Castle Point,local_authority,country/england,,,E07000069,local_authority/Castle Point +local_authority/E07000070,Chelmsford,local_authority,country/england,,,E07000070,local_authority/Chelmsford +local_authority/E07000071,Colchester,local_authority,country/england,,,E07000071,local_authority/Colchester +local_authority/E07000072,Epping Forest,local_authority,country/england,,,E07000072,local_authority/Epping Forest +local_authority/E07000073,Harlow,local_authority,country/england,,,E07000073,local_authority/Harlow +local_authority/E07000074,Maldon,local_authority,country/england,,,E07000074,local_authority/Maldon +local_authority/E07000075,Rochford,local_authority,country/england,,,E07000075,local_authority/Rochford +local_authority/E07000076,Tendring,local_authority,country/england,,,E07000076,local_authority/Tendring +local_authority/E07000077,Uttlesford,local_authority,country/england,,,E07000077,local_authority/Uttlesford +local_authority/E07000078,Cheltenham,local_authority,country/england,,,E07000078,local_authority/Cheltenham +local_authority/E07000079,Cotswold,local_authority,country/england,,,E07000079,local_authority/Cotswold +local_authority/E07000080,Forest of Dean,local_authority,country/england,,,E07000080,local_authority/Forest of Dean +local_authority/E07000081,Gloucester,local_authority,country/england,,,E07000081,local_authority/Gloucester +local_authority/E07000082,Stroud,local_authority,country/england,,,E07000082,local_authority/Stroud +local_authority/E07000083,Tewkesbury,local_authority,country/england,,,E07000083,local_authority/Tewkesbury +local_authority/E07000084,Basingstoke and Deane,local_authority,country/england,,,E07000084,local_authority/Basingstoke and Deane +local_authority/E07000085,East Hampshire,local_authority,country/england,,,E07000085,local_authority/East Hampshire +local_authority/E07000086,Eastleigh,local_authority,country/england,,,E07000086,local_authority/Eastleigh +local_authority/E07000087,Fareham,local_authority,country/england,,,E07000087,local_authority/Fareham +local_authority/E07000088,Gosport,local_authority,country/england,,,E07000088,local_authority/Gosport +local_authority/E07000089,Hart,local_authority,country/england,,,E07000089,local_authority/Hart +local_authority/E07000090,Havant,local_authority,country/england,,,E07000090,local_authority/Havant +local_authority/E07000091,New Forest,local_authority,country/england,,,E07000091,local_authority/New Forest +local_authority/E07000092,Rushmoor,local_authority,country/england,,,E07000092,local_authority/Rushmoor +local_authority/E07000093,Test Valley,local_authority,country/england,,,E07000093,local_authority/Test Valley +local_authority/E07000094,Winchester,local_authority,country/england,,,E07000094,local_authority/Winchester +local_authority/E07000095,Broxbourne,local_authority,country/england,,,E07000095,local_authority/Broxbourne +local_authority/E07000096,Dacorum,local_authority,country/england,,,E07000096,local_authority/Dacorum +local_authority/E07000098,Hertsmere,local_authority,country/england,,,E07000098,local_authority/Hertsmere +local_authority/E07000099,North Hertfordshire,local_authority,country/england,,,E07000099,local_authority/North Hertfordshire +local_authority/E07000102,Three Rivers,local_authority,country/england,,,E07000102,local_authority/Three Rivers +local_authority/E07000103,Watford,local_authority,country/england,,,E07000103,local_authority/Watford +local_authority/E07000105,Ashford,local_authority,country/england,,,E07000105,local_authority/Ashford +local_authority/E07000106,Canterbury,local_authority,country/england,,,E07000106,local_authority/Canterbury +local_authority/E07000107,Dartford,local_authority,country/england,,,E07000107,local_authority/Dartford +local_authority/E07000108,Dover,local_authority,country/england,,,E07000108,local_authority/Dover +local_authority/E07000109,Gravesham,local_authority,country/england,,,E07000109,local_authority/Gravesham +local_authority/E07000110,Maidstone,local_authority,country/england,,,E07000110,local_authority/Maidstone +local_authority/E07000111,Sevenoaks,local_authority,country/england,,,E07000111,local_authority/Sevenoaks +local_authority/E07000112,Folkestone and Hythe,local_authority,country/england,,,E07000112,local_authority/Folkestone and Hythe +local_authority/E07000113,Swale,local_authority,country/england,,,E07000113,local_authority/Swale +local_authority/E07000114,Thanet,local_authority,country/england,,,E07000114,local_authority/Thanet +local_authority/E07000115,Tonbridge and Malling,local_authority,country/england,,,E07000115,local_authority/Tonbridge and Malling +local_authority/E07000116,Tunbridge Wells,local_authority,country/england,,,E07000116,local_authority/Tunbridge Wells +local_authority/E07000117,Burnley,local_authority,country/england,,,E07000117,local_authority/Burnley +local_authority/E07000118,Chorley,local_authority,country/england,,,E07000118,local_authority/Chorley +local_authority/E07000119,Fylde,local_authority,country/england,,,E07000119,local_authority/Fylde +local_authority/E07000120,Hyndburn,local_authority,country/england,,,E07000120,local_authority/Hyndburn +local_authority/E07000121,Lancaster,local_authority,country/england,,,E07000121,local_authority/Lancaster +local_authority/E07000122,Pendle,local_authority,country/england,,,E07000122,local_authority/Pendle +local_authority/E07000123,Preston,local_authority,country/england,,,E07000123,local_authority/Preston +local_authority/E07000124,Ribble Valley,local_authority,country/england,,,E07000124,local_authority/Ribble Valley +local_authority/E07000125,Rossendale,local_authority,country/england,,,E07000125,local_authority/Rossendale +local_authority/E07000126,South Ribble,local_authority,country/england,,,E07000126,local_authority/South Ribble +local_authority/E07000127,West Lancashire,local_authority,country/england,,,E07000127,local_authority/West Lancashire +local_authority/E07000128,Wyre,local_authority,country/england,,,E07000128,local_authority/Wyre +local_authority/E07000129,Blaby,local_authority,country/england,,,E07000129,local_authority/Blaby +local_authority/E07000130,Charnwood,local_authority,country/england,,,E07000130,local_authority/Charnwood +local_authority/E07000131,Harborough,local_authority,country/england,,,E07000131,local_authority/Harborough +local_authority/E07000132,Hinckley and Bosworth,local_authority,country/england,,,E07000132,local_authority/Hinckley and Bosworth +local_authority/E07000133,Melton,local_authority,country/england,,,E07000133,local_authority/Melton +local_authority/E07000134,North West Leicestershire,local_authority,country/england,,,E07000134,local_authority/North West Leicestershire +local_authority/E07000135,Oadby and Wigston,local_authority,country/england,,,E07000135,local_authority/Oadby and Wigston +local_authority/E07000136,Boston,local_authority,country/england,,,E07000136,local_authority/Boston +local_authority/E07000137,East Lindsey,local_authority,country/england,,,E07000137,local_authority/East Lindsey +local_authority/E07000138,Lincoln,local_authority,country/england,,,E07000138,local_authority/Lincoln +local_authority/E07000139,North Kesteven,local_authority,country/england,,,E07000139,local_authority/North Kesteven +local_authority/E07000140,South Holland,local_authority,country/england,,,E07000140,local_authority/South Holland +local_authority/E07000141,South Kesteven,local_authority,country/england,,,E07000141,local_authority/South Kesteven +local_authority/E07000142,West Lindsey,local_authority,country/england,,,E07000142,local_authority/West Lindsey +local_authority/E07000143,Breckland,local_authority,country/england,,,E07000143,local_authority/Breckland +local_authority/E07000144,Broadland,local_authority,country/england,,,E07000144,local_authority/Broadland +local_authority/E07000145,Great Yarmouth,local_authority,country/england,,,E07000145,local_authority/Great Yarmouth +local_authority/E07000146,King's Lynn and West Norfolk,local_authority,country/england,,,E07000146,local_authority/King's Lynn and West Norfolk +local_authority/E07000147,North Norfolk,local_authority,country/england,,,E07000147,local_authority/North Norfolk +local_authority/E07000148,Norwich,local_authority,country/england,,,E07000148,local_authority/Norwich +local_authority/E07000149,South Norfolk,local_authority,country/england,,,E07000149,local_authority/South Norfolk +local_authority/E07000170,Ashfield,local_authority,country/england,,,E07000170,local_authority/Ashfield +local_authority/E07000171,Bassetlaw,local_authority,country/england,,,E07000171,local_authority/Bassetlaw +local_authority/E07000172,Broxtowe,local_authority,country/england,,,E07000172,local_authority/Broxtowe +local_authority/E07000173,Gedling,local_authority,country/england,,,E07000173,local_authority/Gedling +local_authority/E07000174,Mansfield,local_authority,country/england,,,E07000174,local_authority/Mansfield +local_authority/E07000175,Newark and Sherwood,local_authority,country/england,,,E07000175,local_authority/Newark and Sherwood +local_authority/E07000176,Rushcliffe,local_authority,country/england,,,E07000176,local_authority/Rushcliffe +local_authority/E07000177,Cherwell,local_authority,country/england,,,E07000177,local_authority/Cherwell +local_authority/E07000178,Oxford,local_authority,country/england,,,E07000178,local_authority/Oxford +local_authority/E07000179,South Oxfordshire,local_authority,country/england,,,E07000179,local_authority/South Oxfordshire +local_authority/E07000180,Vale of White Horse,local_authority,country/england,,,E07000180,local_authority/Vale of White Horse +local_authority/E07000181,West Oxfordshire,local_authority,country/england,,,E07000181,local_authority/West Oxfordshire +local_authority/E07000192,Cannock Chase,local_authority,country/england,,,E07000192,local_authority/Cannock Chase +local_authority/E07000193,East Staffordshire,local_authority,country/england,,,E07000193,local_authority/East Staffordshire +local_authority/E07000194,Lichfield,local_authority,country/england,,,E07000194,local_authority/Lichfield +local_authority/E07000195,Newcastle-under-Lyme,local_authority,country/england,,,E07000195,local_authority/Newcastle-under-Lyme +local_authority/E07000196,South Staffordshire,local_authority,country/england,,,E07000196,local_authority/South Staffordshire +local_authority/E07000197,Stafford,local_authority,country/england,,,E07000197,local_authority/Stafford +local_authority/E07000198,Staffordshire Moorlands,local_authority,country/england,,,E07000198,local_authority/Staffordshire Moorlands +local_authority/E07000199,Tamworth,local_authority,country/england,,,E07000199,local_authority/Tamworth +local_authority/E07000200,Babergh,local_authority,country/england,,,E07000200,local_authority/Babergh +local_authority/E07000202,Ipswich,local_authority,country/england,,,E07000202,local_authority/Ipswich +local_authority/E07000203,Mid Suffolk,local_authority,country/england,,,E07000203,local_authority/Mid Suffolk +local_authority/E07000207,Elmbridge,local_authority,country/england,,,E07000207,local_authority/Elmbridge +local_authority/E07000208,Epsom and Ewell,local_authority,country/england,,,E07000208,local_authority/Epsom and Ewell +local_authority/E07000209,Guildford,local_authority,country/england,,,E07000209,local_authority/Guildford +local_authority/E07000210,Mole Valley,local_authority,country/england,,,E07000210,local_authority/Mole Valley +local_authority/E07000211,Reigate and Banstead,local_authority,country/england,,,E07000211,local_authority/Reigate and Banstead +local_authority/E07000212,Runnymede,local_authority,country/england,,,E07000212,local_authority/Runnymede +local_authority/E07000213,Spelthorne,local_authority,country/england,,,E07000213,local_authority/Spelthorne +local_authority/E07000214,Surrey Heath,local_authority,country/england,,,E07000214,local_authority/Surrey Heath +local_authority/E07000215,Tandridge,local_authority,country/england,,,E07000215,local_authority/Tandridge +local_authority/E07000216,Waverley,local_authority,country/england,,,E07000216,local_authority/Waverley +local_authority/E07000217,Woking,local_authority,country/england,,,E07000217,local_authority/Woking +local_authority/E07000218,North Warwickshire,local_authority,country/england,,,E07000218,local_authority/North Warwickshire +local_authority/E07000219,Nuneaton and Bedworth,local_authority,country/england,,,E07000219,local_authority/Nuneaton and Bedworth +local_authority/E07000220,Rugby,local_authority,country/england,,,E07000220,local_authority/Rugby +local_authority/E07000221,Stratford-on-Avon,local_authority,country/england,,,E07000221,local_authority/Stratford-on-Avon +local_authority/E07000222,Warwick,local_authority,country/england,,,E07000222,local_authority/Warwick +local_authority/E07000223,Adur,local_authority,country/england,,,E07000223,local_authority/Adur +local_authority/E07000224,Arun,local_authority,country/england,,,E07000224,local_authority/Arun +local_authority/E07000225,Chichester,local_authority,country/england,,,E07000225,local_authority/Chichester +local_authority/E07000226,Crawley,local_authority,country/england,,,E07000226,local_authority/Crawley +local_authority/E07000227,Horsham,local_authority,country/england,,,E07000227,local_authority/Horsham +local_authority/E07000228,Mid Sussex,local_authority,country/england,,,E07000228,local_authority/Mid Sussex +local_authority/E07000229,Worthing,local_authority,country/england,,,E07000229,local_authority/Worthing +local_authority/E07000234,Bromsgrove,local_authority,country/england,,,E07000234,local_authority/Bromsgrove +local_authority/E07000235,Malvern Hills,local_authority,country/england,,,E07000235,local_authority/Malvern Hills +local_authority/E07000236,Redditch,local_authority,country/england,,,E07000236,local_authority/Redditch +local_authority/E07000237,Worcester,local_authority,country/england,,,E07000237,local_authority/Worcester +local_authority/E07000238,Wychavon,local_authority,country/england,,,E07000238,local_authority/Wychavon +local_authority/E07000239,Wyre Forest,local_authority,country/england,,,E07000239,local_authority/Wyre Forest +local_authority/E07000240,St Albans,local_authority,country/england,,,E07000240,local_authority/St Albans +local_authority/E07000241,Welwyn Hatfield,local_authority,country/england,,,E07000241,local_authority/Welwyn Hatfield +local_authority/E07000242,East Hertfordshire,local_authority,country/england,,,E07000242,local_authority/East Hertfordshire +local_authority/E07000243,Stevenage,local_authority,country/england,,,E07000243,local_authority/Stevenage +local_authority/E07000244,East Suffolk,local_authority,country/england,,,E07000244,local_authority/East Suffolk +local_authority/E07000245,West Suffolk,local_authority,country/england,,,E07000245,local_authority/West Suffolk +local_authority/E08000001,Bolton,local_authority,country/england,,,E08000001,local_authority/Bolton +local_authority/E08000002,Bury,local_authority,country/england,,,E08000002,local_authority/Bury +local_authority/E08000003,Manchester,local_authority,country/england,,,E08000003,local_authority/Manchester +local_authority/E08000004,Oldham,local_authority,country/england,,,E08000004,local_authority/Oldham +local_authority/E08000005,Rochdale,local_authority,country/england,,,E08000005,local_authority/Rochdale +local_authority/E08000006,Salford,local_authority,country/england,,,E08000006,local_authority/Salford +local_authority/E08000007,Stockport,local_authority,country/england,,,E08000007,local_authority/Stockport +local_authority/E08000008,Tameside,local_authority,country/england,,,E08000008,local_authority/Tameside +local_authority/E08000009,Trafford,local_authority,country/england,,,E08000009,local_authority/Trafford +local_authority/E08000010,Wigan,local_authority,country/england,,,E08000010,local_authority/Wigan +local_authority/E08000011,Knowsley,local_authority,country/england,,,E08000011,local_authority/Knowsley +local_authority/E08000012,Liverpool,local_authority,country/england,,,E08000012,local_authority/Liverpool +local_authority/E08000013,St. Helens,local_authority,country/england,,,E08000013,local_authority/St. Helens +local_authority/E08000014,Sefton,local_authority,country/england,,,E08000014,local_authority/Sefton +local_authority/E08000015,Wirral,local_authority,country/england,,,E08000015,local_authority/Wirral +local_authority/E08000016,Barnsley,local_authority,country/england,,,E08000016,local_authority/Barnsley +local_authority/E08000017,Doncaster,local_authority,country/england,,,E08000017,local_authority/Doncaster +local_authority/E08000018,Rotherham,local_authority,country/england,,,E08000018,local_authority/Rotherham +local_authority/E08000019,Sheffield,local_authority,country/england,,,E08000019,local_authority/Sheffield +local_authority/E08000021,Newcastle upon Tyne,local_authority,country/england,,,E08000021,local_authority/Newcastle upon Tyne +local_authority/E08000022,North Tyneside,local_authority,country/england,,,E08000022,local_authority/North Tyneside +local_authority/E08000023,South Tyneside,local_authority,country/england,,,E08000023,local_authority/South Tyneside +local_authority/E08000024,Sunderland,local_authority,country/england,,,E08000024,local_authority/Sunderland +local_authority/E08000025,Birmingham,local_authority,country/england,,,E08000025,local_authority/Birmingham +local_authority/E08000026,Coventry,local_authority,country/england,,,E08000026,local_authority/Coventry +local_authority/E08000027,Dudley,local_authority,country/england,,,E08000027,local_authority/Dudley +local_authority/E08000028,Sandwell,local_authority,country/england,,,E08000028,local_authority/Sandwell +local_authority/E08000029,Solihull,local_authority,country/england,,,E08000029,local_authority/Solihull +local_authority/E08000030,Walsall,local_authority,country/england,,,E08000030,local_authority/Walsall +local_authority/E08000031,Wolverhampton,local_authority,country/england,,,E08000031,local_authority/Wolverhampton +local_authority/E08000032,Bradford,local_authority,country/england,,,E08000032,local_authority/Bradford +local_authority/E08000033,Calderdale,local_authority,country/england,,,E08000033,local_authority/Calderdale +local_authority/E08000034,Kirklees,local_authority,country/england,,,E08000034,local_authority/Kirklees +local_authority/E08000035,Leeds,local_authority,country/england,,,E08000035,local_authority/Leeds +local_authority/E08000036,Wakefield,local_authority,country/england,,,E08000036,local_authority/Wakefield +local_authority/E08000037,Gateshead,local_authority,country/england,,,E08000037,local_authority/Gateshead +local_authority/E09000001,City of London,local_authority,country/england,,,E09000001,local_authority/City of London +local_authority/E09000002,Barking and Dagenham,local_authority,country/england,,,E09000002,local_authority/Barking and Dagenham +local_authority/E09000003,Barnet,local_authority,country/england,,,E09000003,local_authority/Barnet +local_authority/E09000004,Bexley,local_authority,country/england,,,E09000004,local_authority/Bexley +local_authority/E09000005,Brent,local_authority,country/england,,,E09000005,local_authority/Brent +local_authority/E09000006,Bromley,local_authority,country/england,,,E09000006,local_authority/Bromley +local_authority/E09000007,Camden,local_authority,country/england,,,E09000007,local_authority/Camden +local_authority/E09000008,Croydon,local_authority,country/england,,,E09000008,local_authority/Croydon +local_authority/E09000009,Ealing,local_authority,country/england,,,E09000009,local_authority/Ealing +local_authority/E09000010,Enfield,local_authority,country/england,,,E09000010,local_authority/Enfield +local_authority/E09000011,Greenwich,local_authority,country/england,,,E09000011,local_authority/Greenwich +local_authority/E09000012,Hackney,local_authority,country/england,,,E09000012,local_authority/Hackney +local_authority/E09000013,Hammersmith and Fulham,local_authority,country/england,,,E09000013,local_authority/Hammersmith and Fulham +local_authority/E09000014,Haringey,local_authority,country/england,,,E09000014,local_authority/Haringey +local_authority/E09000015,Harrow,local_authority,country/england,,,E09000015,local_authority/Harrow +local_authority/E09000016,Havering,local_authority,country/england,,,E09000016,local_authority/Havering +local_authority/E09000017,Hillingdon,local_authority,country/england,,,E09000017,local_authority/Hillingdon +local_authority/E09000018,Hounslow,local_authority,country/england,,,E09000018,local_authority/Hounslow +local_authority/E09000019,Islington,local_authority,country/england,,,E09000019,local_authority/Islington +local_authority/E09000020,Kensington and Chelsea,local_authority,country/england,,,E09000020,local_authority/Kensington and Chelsea +local_authority/E09000021,Kingston upon Thames,local_authority,country/england,,,E09000021,local_authority/Kingston upon Thames +local_authority/E09000022,Lambeth,local_authority,country/england,,,E09000022,local_authority/Lambeth +local_authority/E09000023,Lewisham,local_authority,country/england,,,E09000023,local_authority/Lewisham +local_authority/E09000024,Merton,local_authority,country/england,,,E09000024,local_authority/Merton +local_authority/E09000025,Newham,local_authority,country/england,,,E09000025,local_authority/Newham +local_authority/E09000026,Redbridge,local_authority,country/england,,,E09000026,local_authority/Redbridge +local_authority/E09000027,Richmond upon Thames,local_authority,country/england,,,E09000027,local_authority/Richmond upon Thames +local_authority/E09000028,Southwark,local_authority,country/england,,,E09000028,local_authority/Southwark +local_authority/E09000029,Sutton,local_authority,country/england,,,E09000029,local_authority/Sutton +local_authority/E09000030,Tower Hamlets,local_authority,country/england,,,E09000030,local_authority/Tower Hamlets +local_authority/E09000031,Waltham Forest,local_authority,country/england,,,E09000031,local_authority/Waltham Forest +local_authority/E09000032,Wandsworth,local_authority,country/england,,,E09000032,local_authority/Wandsworth +local_authority/E09000033,Westminster,local_authority,country/england,,,E09000033,local_authority/Westminster +local_authority/N09000001,Antrim and Newtownabbey,local_authority,country/northern_ireland,,,N09000001,local_authority/Antrim and Newtownabbey +local_authority/N09000002,"Armagh City, Banbridge and Craigavon",local_authority,country/northern_ireland,,,N09000002,"local_authority/Armagh City, Banbridge and Craigavon" +local_authority/N09000003,Belfast,local_authority,country/northern_ireland,,,N09000003,local_authority/Belfast +local_authority/N09000004,Causeway Coast and Glens,local_authority,country/northern_ireland,,,N09000004,local_authority/Causeway Coast and Glens +local_authority/N09000005,Derry City and Strabane,local_authority,country/northern_ireland,,,N09000005,local_authority/Derry City and Strabane +local_authority/N09000006,Fermanagh and Omagh,local_authority,country/northern_ireland,,,N09000006,local_authority/Fermanagh and Omagh +local_authority/N09000007,Lisburn and Castlereagh,local_authority,country/northern_ireland,,,N09000007,local_authority/Lisburn and Castlereagh +local_authority/N09000008,Mid and East Antrim,local_authority,country/northern_ireland,,,N09000008,local_authority/Mid and East Antrim +local_authority/N09000009,Mid Ulster,local_authority,country/northern_ireland,,,N09000009,local_authority/Mid Ulster +local_authority/N09000010,"Newry, Mourne and Down",local_authority,country/northern_ireland,,,N09000010,"local_authority/Newry, Mourne and Down" +local_authority/N09000011,Ards and North Down,local_authority,country/northern_ireland,,,N09000011,local_authority/Ards and North Down +local_authority/S12000005,Clackmannanshire,local_authority,country/scotland,,,S12000005,local_authority/Clackmannanshire +local_authority/S12000006,Dumfries and Galloway,local_authority,country/scotland,,,S12000006,local_authority/Dumfries and Galloway +local_authority/S12000008,East Ayrshire,local_authority,country/scotland,,,S12000008,local_authority/East Ayrshire +local_authority/S12000010,East Lothian,local_authority,country/scotland,,,S12000010,local_authority/East Lothian +local_authority/S12000011,East Renfrewshire,local_authority,country/scotland,,,S12000011,local_authority/East Renfrewshire +local_authority/S12000013,Na h-Eileanan Siar,local_authority,country/scotland,,,S12000013,local_authority/Na h-Eileanan Siar +local_authority/S12000014,Falkirk,local_authority,country/scotland,,,S12000014,local_authority/Falkirk +local_authority/S12000017,Highland,local_authority,country/scotland,,,S12000017,local_authority/Highland +local_authority/S12000018,Inverclyde,local_authority,country/scotland,,,S12000018,local_authority/Inverclyde +local_authority/S12000019,Midlothian,local_authority,country/scotland,,,S12000019,local_authority/Midlothian +local_authority/S12000020,Moray,local_authority,country/scotland,,,S12000020,local_authority/Moray +local_authority/S12000021,North Ayrshire,local_authority,country/scotland,,,S12000021,local_authority/North Ayrshire +local_authority/S12000023,Orkney Islands,local_authority,country/scotland,,,S12000023,local_authority/Orkney Islands +local_authority/S12000026,Scottish Borders,local_authority,country/scotland,,,S12000026,local_authority/Scottish Borders +local_authority/S12000027,Shetland Islands,local_authority,country/scotland,,,S12000027,local_authority/Shetland Islands +local_authority/S12000028,South Ayrshire,local_authority,country/scotland,,,S12000028,local_authority/South Ayrshire +local_authority/S12000029,South Lanarkshire,local_authority,country/scotland,,,S12000029,local_authority/South Lanarkshire +local_authority/S12000030,Stirling,local_authority,country/scotland,,,S12000030,local_authority/Stirling +local_authority/S12000033,Aberdeen City,local_authority,country/scotland,,,S12000033,local_authority/Aberdeen City +local_authority/S12000034,Aberdeenshire,local_authority,country/scotland,,,S12000034,local_authority/Aberdeenshire +local_authority/S12000035,Argyll and Bute,local_authority,country/scotland,,,S12000035,local_authority/Argyll and Bute +local_authority/S12000036,City of Edinburgh,local_authority,country/scotland,,,S12000036,local_authority/City of Edinburgh +local_authority/S12000038,Renfrewshire,local_authority,country/scotland,,,S12000038,local_authority/Renfrewshire +local_authority/S12000039,West Dunbartonshire,local_authority,country/scotland,,,S12000039,local_authority/West Dunbartonshire +local_authority/S12000040,West Lothian,local_authority,country/scotland,,,S12000040,local_authority/West Lothian +local_authority/S12000041,Angus,local_authority,country/scotland,,,S12000041,local_authority/Angus +local_authority/S12000042,Dundee City,local_authority,country/scotland,,,S12000042,local_authority/Dundee City +local_authority/S12000045,East Dunbartonshire,local_authority,country/scotland,,,S12000045,local_authority/East Dunbartonshire +local_authority/S12000047,Fife,local_authority,country/scotland,,,S12000047,local_authority/Fife +local_authority/S12000048,Perth and Kinross,local_authority,country/scotland,,,S12000048,local_authority/Perth and Kinross +local_authority/S12000049,Glasgow City,local_authority,country/scotland,,,S12000049,local_authority/Glasgow City +local_authority/S12000050,North Lanarkshire,local_authority,country/scotland,,,S12000050,local_authority/North Lanarkshire +local_authority/W06000001,Isle of Anglesey,local_authority,country/wales,,,W06000001,local_authority/Isle of Anglesey +local_authority/W06000002,Gwynedd,local_authority,country/wales,,,W06000002,local_authority/Gwynedd +local_authority/W06000003,Conwy,local_authority,country/wales,,,W06000003,local_authority/Conwy +local_authority/W06000004,Denbighshire,local_authority,country/wales,,,W06000004,local_authority/Denbighshire +local_authority/W06000005,Flintshire,local_authority,country/wales,,,W06000005,local_authority/Flintshire +local_authority/W06000006,Wrexham,local_authority,country/wales,,,W06000006,local_authority/Wrexham +local_authority/W06000008,Ceredigion,local_authority,country/wales,,,W06000008,local_authority/Ceredigion +local_authority/W06000009,Pembrokeshire,local_authority,country/wales,,,W06000009,local_authority/Pembrokeshire +local_authority/W06000010,Carmarthenshire,local_authority,country/wales,,,W06000010,local_authority/Carmarthenshire +local_authority/W06000011,Swansea,local_authority,country/wales,,,W06000011,local_authority/Swansea +local_authority/W06000012,Neath Port Talbot,local_authority,country/wales,,,W06000012,local_authority/Neath Port Talbot +local_authority/W06000013,Bridgend,local_authority,country/wales,,,W06000013,local_authority/Bridgend +local_authority/W06000014,Vale of Glamorgan,local_authority,country/wales,,,W06000014,local_authority/Vale of Glamorgan +local_authority/W06000015,Cardiff,local_authority,country/wales,,,W06000015,local_authority/Cardiff +local_authority/W06000016,Rhondda Cynon Taf,local_authority,country/wales,,,W06000016,local_authority/Rhondda Cynon Taf +local_authority/W06000018,Caerphilly,local_authority,country/wales,,,W06000018,local_authority/Caerphilly +local_authority/W06000019,Blaenau Gwent,local_authority,country/wales,,,W06000019,local_authority/Blaenau Gwent +local_authority/W06000020,Torfaen,local_authority,country/wales,,,W06000020,local_authority/Torfaen +local_authority/W06000021,Monmouthshire,local_authority,country/wales,,,W06000021,local_authority/Monmouthshire +local_authority/W06000022,Newport,local_authority,country/wales,,,W06000022,local_authority/Newport +local_authority/W06000023,Powys,local_authority,country/wales,,,W06000023,local_authority/Powys +local_authority/W06000024,Merthyr Tydfil,local_authority,country/wales,,,W06000024,local_authority/Merthyr Tydfil diff --git a/policyengine_api/data/v2/catalog/region_reference_files/us.csv b/policyengine_api/data/v2/catalog/region_reference_files/us.csv new file mode 100644 index 000000000..a8a3b3166 --- /dev/null +++ b/policyengine_api/data/v2/catalog/region_reference_files/us.csv @@ -0,0 +1,822 @@ +code,label,region_type,parent_code,state_code,state_name,filter_value,v1_code +us,United States,national,,,,,us +state/ak,Alaska,state,us,AK,Alaska,2,state/ak +state/al,Alabama,state,us,AL,Alabama,1,state/al +state/ar,Arkansas,state,us,AR,Arkansas,5,state/ar +state/az,Arizona,state,us,AZ,Arizona,4,state/az +state/ca,California,state,us,CA,California,6,state/ca +state/co,Colorado,state,us,CO,Colorado,8,state/co +state/ct,Connecticut,state,us,CT,Connecticut,9,state/ct +state/dc,District of Columbia,state,us,DC,District of Columbia,11,state/dc +state/de,Delaware,state,us,DE,Delaware,10,state/de +state/fl,Florida,state,us,FL,Florida,12,state/fl +state/ga,Georgia,state,us,GA,Georgia,13,state/ga +state/hi,Hawaii,state,us,HI,Hawaii,15,state/hi +state/ia,Iowa,state,us,IA,Iowa,19,state/ia +state/id,Idaho,state,us,ID,Idaho,16,state/id +state/il,Illinois,state,us,IL,Illinois,17,state/il +state/in,Indiana,state,us,IN,Indiana,18,state/in +state/ks,Kansas,state,us,KS,Kansas,20,state/ks +state/ky,Kentucky,state,us,KY,Kentucky,21,state/ky +state/la,Louisiana,state,us,LA,Louisiana,22,state/la +state/ma,Massachusetts,state,us,MA,Massachusetts,25,state/ma +state/md,Maryland,state,us,MD,Maryland,24,state/md +state/me,Maine,state,us,ME,Maine,23,state/me +state/mi,Michigan,state,us,MI,Michigan,26,state/mi +state/mn,Minnesota,state,us,MN,Minnesota,27,state/mn +state/mo,Missouri,state,us,MO,Missouri,29,state/mo +state/ms,Mississippi,state,us,MS,Mississippi,28,state/ms +state/mt,Montana,state,us,MT,Montana,30,state/mt +state/nc,North Carolina,state,us,NC,North Carolina,37,state/nc +state/nd,North Dakota,state,us,ND,North Dakota,38,state/nd +state/ne,Nebraska,state,us,NE,Nebraska,31,state/ne +state/nh,New Hampshire,state,us,NH,New Hampshire,33,state/nh +state/nj,New Jersey,state,us,NJ,New Jersey,34,state/nj +state/nm,New Mexico,state,us,NM,New Mexico,35,state/nm +state/nv,Nevada,state,us,NV,Nevada,32,state/nv +state/ny,New York,state,us,NY,New York,36,state/ny +state/oh,Ohio,state,us,OH,Ohio,39,state/oh +state/ok,Oklahoma,state,us,OK,Oklahoma,40,state/ok +state/or,Oregon,state,us,OR,Oregon,41,state/or +state/pa,Pennsylvania,state,us,PA,Pennsylvania,42,state/pa +state/ri,Rhode Island,state,us,RI,Rhode Island,44,state/ri +state/sc,South Carolina,state,us,SC,South Carolina,45,state/sc +state/sd,South Dakota,state,us,SD,South Dakota,46,state/sd +state/tn,Tennessee,state,us,TN,Tennessee,47,state/tn +state/tx,Texas,state,us,TX,Texas,48,state/tx +state/ut,Utah,state,us,UT,Utah,49,state/ut +state/va,Virginia,state,us,VA,Virginia,51,state/va +state/vt,Vermont,state,us,VT,Vermont,50,state/vt +state/wa,Washington,state,us,WA,Washington,53,state/wa +state/wi,Wisconsin,state,us,WI,Wisconsin,55,state/wi +state/wv,West Virginia,state,us,WV,West Virginia,54,state/wv +state/wy,Wyoming,state,us,WY,Wyoming,56,state/wy +congressional_district/AK-01,Alaska's at-large congressional district,congressional_district,state/ak,AK,Alaska,200,congressional_district/AK-01 +congressional_district/AL-01,Alabama's 1st congressional district,congressional_district,state/al,AL,Alabama,101,congressional_district/AL-01 +congressional_district/AL-02,Alabama's 2nd congressional district,congressional_district,state/al,AL,Alabama,102,congressional_district/AL-02 +congressional_district/AL-03,Alabama's 3rd congressional district,congressional_district,state/al,AL,Alabama,103,congressional_district/AL-03 +congressional_district/AL-04,Alabama's 4th congressional district,congressional_district,state/al,AL,Alabama,104,congressional_district/AL-04 +congressional_district/AL-05,Alabama's 5th congressional district,congressional_district,state/al,AL,Alabama,105,congressional_district/AL-05 +congressional_district/AL-06,Alabama's 6th congressional district,congressional_district,state/al,AL,Alabama,106,congressional_district/AL-06 +congressional_district/AL-07,Alabama's 7th congressional district,congressional_district,state/al,AL,Alabama,107,congressional_district/AL-07 +congressional_district/AR-01,Arkansas's 1st congressional district,congressional_district,state/ar,AR,Arkansas,501,congressional_district/AR-01 +congressional_district/AR-02,Arkansas's 2nd congressional district,congressional_district,state/ar,AR,Arkansas,502,congressional_district/AR-02 +congressional_district/AR-03,Arkansas's 3rd congressional district,congressional_district,state/ar,AR,Arkansas,503,congressional_district/AR-03 +congressional_district/AR-04,Arkansas's 4th congressional district,congressional_district,state/ar,AR,Arkansas,504,congressional_district/AR-04 +congressional_district/AZ-01,Arizona's 1st congressional district,congressional_district,state/az,AZ,Arizona,401,congressional_district/AZ-01 +congressional_district/AZ-02,Arizona's 2nd congressional district,congressional_district,state/az,AZ,Arizona,402,congressional_district/AZ-02 +congressional_district/AZ-03,Arizona's 3rd congressional district,congressional_district,state/az,AZ,Arizona,403,congressional_district/AZ-03 +congressional_district/AZ-04,Arizona's 4th congressional district,congressional_district,state/az,AZ,Arizona,404,congressional_district/AZ-04 +congressional_district/AZ-05,Arizona's 5th congressional district,congressional_district,state/az,AZ,Arizona,405,congressional_district/AZ-05 +congressional_district/AZ-06,Arizona's 6th congressional district,congressional_district,state/az,AZ,Arizona,406,congressional_district/AZ-06 +congressional_district/AZ-07,Arizona's 7th congressional district,congressional_district,state/az,AZ,Arizona,407,congressional_district/AZ-07 +congressional_district/AZ-08,Arizona's 8th congressional district,congressional_district,state/az,AZ,Arizona,408,congressional_district/AZ-08 +congressional_district/AZ-09,Arizona's 9th congressional district,congressional_district,state/az,AZ,Arizona,409,congressional_district/AZ-09 +congressional_district/CA-01,California's 1st congressional district,congressional_district,state/ca,CA,California,601,congressional_district/CA-01 +congressional_district/CA-02,California's 2nd congressional district,congressional_district,state/ca,CA,California,602,congressional_district/CA-02 +congressional_district/CA-03,California's 3rd congressional district,congressional_district,state/ca,CA,California,603,congressional_district/CA-03 +congressional_district/CA-04,California's 4th congressional district,congressional_district,state/ca,CA,California,604,congressional_district/CA-04 +congressional_district/CA-05,California's 5th congressional district,congressional_district,state/ca,CA,California,605,congressional_district/CA-05 +congressional_district/CA-06,California's 6th congressional district,congressional_district,state/ca,CA,California,606,congressional_district/CA-06 +congressional_district/CA-07,California's 7th congressional district,congressional_district,state/ca,CA,California,607,congressional_district/CA-07 +congressional_district/CA-08,California's 8th congressional district,congressional_district,state/ca,CA,California,608,congressional_district/CA-08 +congressional_district/CA-09,California's 9th congressional district,congressional_district,state/ca,CA,California,609,congressional_district/CA-09 +congressional_district/CA-10,California's 10th congressional district,congressional_district,state/ca,CA,California,610,congressional_district/CA-10 +congressional_district/CA-11,California's 11th congressional district,congressional_district,state/ca,CA,California,611,congressional_district/CA-11 +congressional_district/CA-12,California's 12th congressional district,congressional_district,state/ca,CA,California,612,congressional_district/CA-12 +congressional_district/CA-13,California's 13th congressional district,congressional_district,state/ca,CA,California,613,congressional_district/CA-13 +congressional_district/CA-14,California's 14th congressional district,congressional_district,state/ca,CA,California,614,congressional_district/CA-14 +congressional_district/CA-15,California's 15th congressional district,congressional_district,state/ca,CA,California,615,congressional_district/CA-15 +congressional_district/CA-16,California's 16th congressional district,congressional_district,state/ca,CA,California,616,congressional_district/CA-16 +congressional_district/CA-17,California's 17th congressional district,congressional_district,state/ca,CA,California,617,congressional_district/CA-17 +congressional_district/CA-18,California's 18th congressional district,congressional_district,state/ca,CA,California,618,congressional_district/CA-18 +congressional_district/CA-19,California's 19th congressional district,congressional_district,state/ca,CA,California,619,congressional_district/CA-19 +congressional_district/CA-20,California's 20th congressional district,congressional_district,state/ca,CA,California,620,congressional_district/CA-20 +congressional_district/CA-21,California's 21st congressional district,congressional_district,state/ca,CA,California,621,congressional_district/CA-21 +congressional_district/CA-22,California's 22nd congressional district,congressional_district,state/ca,CA,California,622,congressional_district/CA-22 +congressional_district/CA-23,California's 23rd congressional district,congressional_district,state/ca,CA,California,623,congressional_district/CA-23 +congressional_district/CA-24,California's 24th congressional district,congressional_district,state/ca,CA,California,624,congressional_district/CA-24 +congressional_district/CA-25,California's 25th congressional district,congressional_district,state/ca,CA,California,625,congressional_district/CA-25 +congressional_district/CA-26,California's 26th congressional district,congressional_district,state/ca,CA,California,626,congressional_district/CA-26 +congressional_district/CA-27,California's 27th congressional district,congressional_district,state/ca,CA,California,627,congressional_district/CA-27 +congressional_district/CA-28,California's 28th congressional district,congressional_district,state/ca,CA,California,628,congressional_district/CA-28 +congressional_district/CA-29,California's 29th congressional district,congressional_district,state/ca,CA,California,629,congressional_district/CA-29 +congressional_district/CA-30,California's 30th congressional district,congressional_district,state/ca,CA,California,630,congressional_district/CA-30 +congressional_district/CA-31,California's 31st congressional district,congressional_district,state/ca,CA,California,631,congressional_district/CA-31 +congressional_district/CA-32,California's 32nd congressional district,congressional_district,state/ca,CA,California,632,congressional_district/CA-32 +congressional_district/CA-33,California's 33rd congressional district,congressional_district,state/ca,CA,California,633,congressional_district/CA-33 +congressional_district/CA-34,California's 34th congressional district,congressional_district,state/ca,CA,California,634,congressional_district/CA-34 +congressional_district/CA-35,California's 35th congressional district,congressional_district,state/ca,CA,California,635,congressional_district/CA-35 +congressional_district/CA-36,California's 36th congressional district,congressional_district,state/ca,CA,California,636,congressional_district/CA-36 +congressional_district/CA-37,California's 37th congressional district,congressional_district,state/ca,CA,California,637,congressional_district/CA-37 +congressional_district/CA-38,California's 38th congressional district,congressional_district,state/ca,CA,California,638,congressional_district/CA-38 +congressional_district/CA-39,California's 39th congressional district,congressional_district,state/ca,CA,California,639,congressional_district/CA-39 +congressional_district/CA-40,California's 40th congressional district,congressional_district,state/ca,CA,California,640,congressional_district/CA-40 +congressional_district/CA-41,California's 41st congressional district,congressional_district,state/ca,CA,California,641,congressional_district/CA-41 +congressional_district/CA-42,California's 42nd congressional district,congressional_district,state/ca,CA,California,642,congressional_district/CA-42 +congressional_district/CA-43,California's 43rd congressional district,congressional_district,state/ca,CA,California,643,congressional_district/CA-43 +congressional_district/CA-44,California's 44th congressional district,congressional_district,state/ca,CA,California,644,congressional_district/CA-44 +congressional_district/CA-45,California's 45th congressional district,congressional_district,state/ca,CA,California,645,congressional_district/CA-45 +congressional_district/CA-46,California's 46th congressional district,congressional_district,state/ca,CA,California,646,congressional_district/CA-46 +congressional_district/CA-47,California's 47th congressional district,congressional_district,state/ca,CA,California,647,congressional_district/CA-47 +congressional_district/CA-48,California's 48th congressional district,congressional_district,state/ca,CA,California,648,congressional_district/CA-48 +congressional_district/CA-49,California's 49th congressional district,congressional_district,state/ca,CA,California,649,congressional_district/CA-49 +congressional_district/CA-50,California's 50th congressional district,congressional_district,state/ca,CA,California,650,congressional_district/CA-50 +congressional_district/CA-51,California's 51st congressional district,congressional_district,state/ca,CA,California,651,congressional_district/CA-51 +congressional_district/CA-52,California's 52nd congressional district,congressional_district,state/ca,CA,California,652,congressional_district/CA-52 +congressional_district/CO-01,Colorado's 1st congressional district,congressional_district,state/co,CO,Colorado,801,congressional_district/CO-01 +congressional_district/CO-02,Colorado's 2nd congressional district,congressional_district,state/co,CO,Colorado,802,congressional_district/CO-02 +congressional_district/CO-03,Colorado's 3rd congressional district,congressional_district,state/co,CO,Colorado,803,congressional_district/CO-03 +congressional_district/CO-04,Colorado's 4th congressional district,congressional_district,state/co,CO,Colorado,804,congressional_district/CO-04 +congressional_district/CO-05,Colorado's 5th congressional district,congressional_district,state/co,CO,Colorado,805,congressional_district/CO-05 +congressional_district/CO-06,Colorado's 6th congressional district,congressional_district,state/co,CO,Colorado,806,congressional_district/CO-06 +congressional_district/CO-07,Colorado's 7th congressional district,congressional_district,state/co,CO,Colorado,807,congressional_district/CO-07 +congressional_district/CO-08,Colorado's 8th congressional district,congressional_district,state/co,CO,Colorado,808,congressional_district/CO-08 +congressional_district/CT-01,Connecticut's 1st congressional district,congressional_district,state/ct,CT,Connecticut,901,congressional_district/CT-01 +congressional_district/CT-02,Connecticut's 2nd congressional district,congressional_district,state/ct,CT,Connecticut,902,congressional_district/CT-02 +congressional_district/CT-03,Connecticut's 3rd congressional district,congressional_district,state/ct,CT,Connecticut,903,congressional_district/CT-03 +congressional_district/CT-04,Connecticut's 4th congressional district,congressional_district,state/ct,CT,Connecticut,904,congressional_district/CT-04 +congressional_district/CT-05,Connecticut's 5th congressional district,congressional_district,state/ct,CT,Connecticut,905,congressional_district/CT-05 +congressional_district/DC-01,District of Columbia's at-large congressional district,congressional_district,state/dc,DC,District of Columbia,1100,congressional_district/DC-01 +congressional_district/DE-01,Delaware's at-large congressional district,congressional_district,state/de,DE,Delaware,1000,congressional_district/DE-01 +congressional_district/FL-01,Florida's 1st congressional district,congressional_district,state/fl,FL,Florida,1201,congressional_district/FL-01 +congressional_district/FL-02,Florida's 2nd congressional district,congressional_district,state/fl,FL,Florida,1202,congressional_district/FL-02 +congressional_district/FL-03,Florida's 3rd congressional district,congressional_district,state/fl,FL,Florida,1203,congressional_district/FL-03 +congressional_district/FL-04,Florida's 4th congressional district,congressional_district,state/fl,FL,Florida,1204,congressional_district/FL-04 +congressional_district/FL-05,Florida's 5th congressional district,congressional_district,state/fl,FL,Florida,1205,congressional_district/FL-05 +congressional_district/FL-06,Florida's 6th congressional district,congressional_district,state/fl,FL,Florida,1206,congressional_district/FL-06 +congressional_district/FL-07,Florida's 7th congressional district,congressional_district,state/fl,FL,Florida,1207,congressional_district/FL-07 +congressional_district/FL-08,Florida's 8th congressional district,congressional_district,state/fl,FL,Florida,1208,congressional_district/FL-08 +congressional_district/FL-09,Florida's 9th congressional district,congressional_district,state/fl,FL,Florida,1209,congressional_district/FL-09 +congressional_district/FL-10,Florida's 10th congressional district,congressional_district,state/fl,FL,Florida,1210,congressional_district/FL-10 +congressional_district/FL-11,Florida's 11th congressional district,congressional_district,state/fl,FL,Florida,1211,congressional_district/FL-11 +congressional_district/FL-12,Florida's 12th congressional district,congressional_district,state/fl,FL,Florida,1212,congressional_district/FL-12 +congressional_district/FL-13,Florida's 13th congressional district,congressional_district,state/fl,FL,Florida,1213,congressional_district/FL-13 +congressional_district/FL-14,Florida's 14th congressional district,congressional_district,state/fl,FL,Florida,1214,congressional_district/FL-14 +congressional_district/FL-15,Florida's 15th congressional district,congressional_district,state/fl,FL,Florida,1215,congressional_district/FL-15 +congressional_district/FL-16,Florida's 16th congressional district,congressional_district,state/fl,FL,Florida,1216,congressional_district/FL-16 +congressional_district/FL-17,Florida's 17th congressional district,congressional_district,state/fl,FL,Florida,1217,congressional_district/FL-17 +congressional_district/FL-18,Florida's 18th congressional district,congressional_district,state/fl,FL,Florida,1218,congressional_district/FL-18 +congressional_district/FL-19,Florida's 19th congressional district,congressional_district,state/fl,FL,Florida,1219,congressional_district/FL-19 +congressional_district/FL-20,Florida's 20th congressional district,congressional_district,state/fl,FL,Florida,1220,congressional_district/FL-20 +congressional_district/FL-21,Florida's 21st congressional district,congressional_district,state/fl,FL,Florida,1221,congressional_district/FL-21 +congressional_district/FL-22,Florida's 22nd congressional district,congressional_district,state/fl,FL,Florida,1222,congressional_district/FL-22 +congressional_district/FL-23,Florida's 23rd congressional district,congressional_district,state/fl,FL,Florida,1223,congressional_district/FL-23 +congressional_district/FL-24,Florida's 24th congressional district,congressional_district,state/fl,FL,Florida,1224,congressional_district/FL-24 +congressional_district/FL-25,Florida's 25th congressional district,congressional_district,state/fl,FL,Florida,1225,congressional_district/FL-25 +congressional_district/FL-26,Florida's 26th congressional district,congressional_district,state/fl,FL,Florida,1226,congressional_district/FL-26 +congressional_district/FL-27,Florida's 27th congressional district,congressional_district,state/fl,FL,Florida,1227,congressional_district/FL-27 +congressional_district/FL-28,Florida's 28th congressional district,congressional_district,state/fl,FL,Florida,1228,congressional_district/FL-28 +congressional_district/GA-01,Georgia's 1st congressional district,congressional_district,state/ga,GA,Georgia,1301,congressional_district/GA-01 +congressional_district/GA-02,Georgia's 2nd congressional district,congressional_district,state/ga,GA,Georgia,1302,congressional_district/GA-02 +congressional_district/GA-03,Georgia's 3rd congressional district,congressional_district,state/ga,GA,Georgia,1303,congressional_district/GA-03 +congressional_district/GA-04,Georgia's 4th congressional district,congressional_district,state/ga,GA,Georgia,1304,congressional_district/GA-04 +congressional_district/GA-05,Georgia's 5th congressional district,congressional_district,state/ga,GA,Georgia,1305,congressional_district/GA-05 +congressional_district/GA-06,Georgia's 6th congressional district,congressional_district,state/ga,GA,Georgia,1306,congressional_district/GA-06 +congressional_district/GA-07,Georgia's 7th congressional district,congressional_district,state/ga,GA,Georgia,1307,congressional_district/GA-07 +congressional_district/GA-08,Georgia's 8th congressional district,congressional_district,state/ga,GA,Georgia,1308,congressional_district/GA-08 +congressional_district/GA-09,Georgia's 9th congressional district,congressional_district,state/ga,GA,Georgia,1309,congressional_district/GA-09 +congressional_district/GA-10,Georgia's 10th congressional district,congressional_district,state/ga,GA,Georgia,1310,congressional_district/GA-10 +congressional_district/GA-11,Georgia's 11th congressional district,congressional_district,state/ga,GA,Georgia,1311,congressional_district/GA-11 +congressional_district/GA-12,Georgia's 12th congressional district,congressional_district,state/ga,GA,Georgia,1312,congressional_district/GA-12 +congressional_district/GA-13,Georgia's 13th congressional district,congressional_district,state/ga,GA,Georgia,1313,congressional_district/GA-13 +congressional_district/GA-14,Georgia's 14th congressional district,congressional_district,state/ga,GA,Georgia,1314,congressional_district/GA-14 +congressional_district/HI-01,Hawaii's 1st congressional district,congressional_district,state/hi,HI,Hawaii,1501,congressional_district/HI-01 +congressional_district/HI-02,Hawaii's 2nd congressional district,congressional_district,state/hi,HI,Hawaii,1502,congressional_district/HI-02 +congressional_district/IA-01,Iowa's 1st congressional district,congressional_district,state/ia,IA,Iowa,1901,congressional_district/IA-01 +congressional_district/IA-02,Iowa's 2nd congressional district,congressional_district,state/ia,IA,Iowa,1902,congressional_district/IA-02 +congressional_district/IA-03,Iowa's 3rd congressional district,congressional_district,state/ia,IA,Iowa,1903,congressional_district/IA-03 +congressional_district/IA-04,Iowa's 4th congressional district,congressional_district,state/ia,IA,Iowa,1904,congressional_district/IA-04 +congressional_district/ID-01,Idaho's 1st congressional district,congressional_district,state/id,ID,Idaho,1601,congressional_district/ID-01 +congressional_district/ID-02,Idaho's 2nd congressional district,congressional_district,state/id,ID,Idaho,1602,congressional_district/ID-02 +congressional_district/IL-01,Illinois's 1st congressional district,congressional_district,state/il,IL,Illinois,1701,congressional_district/IL-01 +congressional_district/IL-02,Illinois's 2nd congressional district,congressional_district,state/il,IL,Illinois,1702,congressional_district/IL-02 +congressional_district/IL-03,Illinois's 3rd congressional district,congressional_district,state/il,IL,Illinois,1703,congressional_district/IL-03 +congressional_district/IL-04,Illinois's 4th congressional district,congressional_district,state/il,IL,Illinois,1704,congressional_district/IL-04 +congressional_district/IL-05,Illinois's 5th congressional district,congressional_district,state/il,IL,Illinois,1705,congressional_district/IL-05 +congressional_district/IL-06,Illinois's 6th congressional district,congressional_district,state/il,IL,Illinois,1706,congressional_district/IL-06 +congressional_district/IL-07,Illinois's 7th congressional district,congressional_district,state/il,IL,Illinois,1707,congressional_district/IL-07 +congressional_district/IL-08,Illinois's 8th congressional district,congressional_district,state/il,IL,Illinois,1708,congressional_district/IL-08 +congressional_district/IL-09,Illinois's 9th congressional district,congressional_district,state/il,IL,Illinois,1709,congressional_district/IL-09 +congressional_district/IL-10,Illinois's 10th congressional district,congressional_district,state/il,IL,Illinois,1710,congressional_district/IL-10 +congressional_district/IL-11,Illinois's 11th congressional district,congressional_district,state/il,IL,Illinois,1711,congressional_district/IL-11 +congressional_district/IL-12,Illinois's 12th congressional district,congressional_district,state/il,IL,Illinois,1712,congressional_district/IL-12 +congressional_district/IL-13,Illinois's 13th congressional district,congressional_district,state/il,IL,Illinois,1713,congressional_district/IL-13 +congressional_district/IL-14,Illinois's 14th congressional district,congressional_district,state/il,IL,Illinois,1714,congressional_district/IL-14 +congressional_district/IL-15,Illinois's 15th congressional district,congressional_district,state/il,IL,Illinois,1715,congressional_district/IL-15 +congressional_district/IL-16,Illinois's 16th congressional district,congressional_district,state/il,IL,Illinois,1716,congressional_district/IL-16 +congressional_district/IL-17,Illinois's 17th congressional district,congressional_district,state/il,IL,Illinois,1717,congressional_district/IL-17 +congressional_district/IN-01,Indiana's 1st congressional district,congressional_district,state/in,IN,Indiana,1801,congressional_district/IN-01 +congressional_district/IN-02,Indiana's 2nd congressional district,congressional_district,state/in,IN,Indiana,1802,congressional_district/IN-02 +congressional_district/IN-03,Indiana's 3rd congressional district,congressional_district,state/in,IN,Indiana,1803,congressional_district/IN-03 +congressional_district/IN-04,Indiana's 4th congressional district,congressional_district,state/in,IN,Indiana,1804,congressional_district/IN-04 +congressional_district/IN-05,Indiana's 5th congressional district,congressional_district,state/in,IN,Indiana,1805,congressional_district/IN-05 +congressional_district/IN-06,Indiana's 6th congressional district,congressional_district,state/in,IN,Indiana,1806,congressional_district/IN-06 +congressional_district/IN-07,Indiana's 7th congressional district,congressional_district,state/in,IN,Indiana,1807,congressional_district/IN-07 +congressional_district/IN-08,Indiana's 8th congressional district,congressional_district,state/in,IN,Indiana,1808,congressional_district/IN-08 +congressional_district/IN-09,Indiana's 9th congressional district,congressional_district,state/in,IN,Indiana,1809,congressional_district/IN-09 +congressional_district/KS-01,Kansas's 1st congressional district,congressional_district,state/ks,KS,Kansas,2001,congressional_district/KS-01 +congressional_district/KS-02,Kansas's 2nd congressional district,congressional_district,state/ks,KS,Kansas,2002,congressional_district/KS-02 +congressional_district/KS-03,Kansas's 3rd congressional district,congressional_district,state/ks,KS,Kansas,2003,congressional_district/KS-03 +congressional_district/KS-04,Kansas's 4th congressional district,congressional_district,state/ks,KS,Kansas,2004,congressional_district/KS-04 +congressional_district/KY-01,Kentucky's 1st congressional district,congressional_district,state/ky,KY,Kentucky,2101,congressional_district/KY-01 +congressional_district/KY-02,Kentucky's 2nd congressional district,congressional_district,state/ky,KY,Kentucky,2102,congressional_district/KY-02 +congressional_district/KY-03,Kentucky's 3rd congressional district,congressional_district,state/ky,KY,Kentucky,2103,congressional_district/KY-03 +congressional_district/KY-04,Kentucky's 4th congressional district,congressional_district,state/ky,KY,Kentucky,2104,congressional_district/KY-04 +congressional_district/KY-05,Kentucky's 5th congressional district,congressional_district,state/ky,KY,Kentucky,2105,congressional_district/KY-05 +congressional_district/KY-06,Kentucky's 6th congressional district,congressional_district,state/ky,KY,Kentucky,2106,congressional_district/KY-06 +congressional_district/LA-01,Louisiana's 1st congressional district,congressional_district,state/la,LA,Louisiana,2201,congressional_district/LA-01 +congressional_district/LA-02,Louisiana's 2nd congressional district,congressional_district,state/la,LA,Louisiana,2202,congressional_district/LA-02 +congressional_district/LA-03,Louisiana's 3rd congressional district,congressional_district,state/la,LA,Louisiana,2203,congressional_district/LA-03 +congressional_district/LA-04,Louisiana's 4th congressional district,congressional_district,state/la,LA,Louisiana,2204,congressional_district/LA-04 +congressional_district/LA-05,Louisiana's 5th congressional district,congressional_district,state/la,LA,Louisiana,2205,congressional_district/LA-05 +congressional_district/LA-06,Louisiana's 6th congressional district,congressional_district,state/la,LA,Louisiana,2206,congressional_district/LA-06 +congressional_district/MA-01,Massachusetts's 1st congressional district,congressional_district,state/ma,MA,Massachusetts,2501,congressional_district/MA-01 +congressional_district/MA-02,Massachusetts's 2nd congressional district,congressional_district,state/ma,MA,Massachusetts,2502,congressional_district/MA-02 +congressional_district/MA-03,Massachusetts's 3rd congressional district,congressional_district,state/ma,MA,Massachusetts,2503,congressional_district/MA-03 +congressional_district/MA-04,Massachusetts's 4th congressional district,congressional_district,state/ma,MA,Massachusetts,2504,congressional_district/MA-04 +congressional_district/MA-05,Massachusetts's 5th congressional district,congressional_district,state/ma,MA,Massachusetts,2505,congressional_district/MA-05 +congressional_district/MA-06,Massachusetts's 6th congressional district,congressional_district,state/ma,MA,Massachusetts,2506,congressional_district/MA-06 +congressional_district/MA-07,Massachusetts's 7th congressional district,congressional_district,state/ma,MA,Massachusetts,2507,congressional_district/MA-07 +congressional_district/MA-08,Massachusetts's 8th congressional district,congressional_district,state/ma,MA,Massachusetts,2508,congressional_district/MA-08 +congressional_district/MA-09,Massachusetts's 9th congressional district,congressional_district,state/ma,MA,Massachusetts,2509,congressional_district/MA-09 +congressional_district/MD-01,Maryland's 1st congressional district,congressional_district,state/md,MD,Maryland,2401,congressional_district/MD-01 +congressional_district/MD-02,Maryland's 2nd congressional district,congressional_district,state/md,MD,Maryland,2402,congressional_district/MD-02 +congressional_district/MD-03,Maryland's 3rd congressional district,congressional_district,state/md,MD,Maryland,2403,congressional_district/MD-03 +congressional_district/MD-04,Maryland's 4th congressional district,congressional_district,state/md,MD,Maryland,2404,congressional_district/MD-04 +congressional_district/MD-05,Maryland's 5th congressional district,congressional_district,state/md,MD,Maryland,2405,congressional_district/MD-05 +congressional_district/MD-06,Maryland's 6th congressional district,congressional_district,state/md,MD,Maryland,2406,congressional_district/MD-06 +congressional_district/MD-07,Maryland's 7th congressional district,congressional_district,state/md,MD,Maryland,2407,congressional_district/MD-07 +congressional_district/MD-08,Maryland's 8th congressional district,congressional_district,state/md,MD,Maryland,2408,congressional_district/MD-08 +congressional_district/ME-01,Maine's 1st congressional district,congressional_district,state/me,ME,Maine,2301,congressional_district/ME-01 +congressional_district/ME-02,Maine's 2nd congressional district,congressional_district,state/me,ME,Maine,2302,congressional_district/ME-02 +congressional_district/MI-01,Michigan's 1st congressional district,congressional_district,state/mi,MI,Michigan,2601,congressional_district/MI-01 +congressional_district/MI-02,Michigan's 2nd congressional district,congressional_district,state/mi,MI,Michigan,2602,congressional_district/MI-02 +congressional_district/MI-03,Michigan's 3rd congressional district,congressional_district,state/mi,MI,Michigan,2603,congressional_district/MI-03 +congressional_district/MI-04,Michigan's 4th congressional district,congressional_district,state/mi,MI,Michigan,2604,congressional_district/MI-04 +congressional_district/MI-05,Michigan's 5th congressional district,congressional_district,state/mi,MI,Michigan,2605,congressional_district/MI-05 +congressional_district/MI-06,Michigan's 6th congressional district,congressional_district,state/mi,MI,Michigan,2606,congressional_district/MI-06 +congressional_district/MI-07,Michigan's 7th congressional district,congressional_district,state/mi,MI,Michigan,2607,congressional_district/MI-07 +congressional_district/MI-08,Michigan's 8th congressional district,congressional_district,state/mi,MI,Michigan,2608,congressional_district/MI-08 +congressional_district/MI-09,Michigan's 9th congressional district,congressional_district,state/mi,MI,Michigan,2609,congressional_district/MI-09 +congressional_district/MI-10,Michigan's 10th congressional district,congressional_district,state/mi,MI,Michigan,2610,congressional_district/MI-10 +congressional_district/MI-11,Michigan's 11th congressional district,congressional_district,state/mi,MI,Michigan,2611,congressional_district/MI-11 +congressional_district/MI-12,Michigan's 12th congressional district,congressional_district,state/mi,MI,Michigan,2612,congressional_district/MI-12 +congressional_district/MI-13,Michigan's 13th congressional district,congressional_district,state/mi,MI,Michigan,2613,congressional_district/MI-13 +congressional_district/MN-01,Minnesota's 1st congressional district,congressional_district,state/mn,MN,Minnesota,2701,congressional_district/MN-01 +congressional_district/MN-02,Minnesota's 2nd congressional district,congressional_district,state/mn,MN,Minnesota,2702,congressional_district/MN-02 +congressional_district/MN-03,Minnesota's 3rd congressional district,congressional_district,state/mn,MN,Minnesota,2703,congressional_district/MN-03 +congressional_district/MN-04,Minnesota's 4th congressional district,congressional_district,state/mn,MN,Minnesota,2704,congressional_district/MN-04 +congressional_district/MN-05,Minnesota's 5th congressional district,congressional_district,state/mn,MN,Minnesota,2705,congressional_district/MN-05 +congressional_district/MN-06,Minnesota's 6th congressional district,congressional_district,state/mn,MN,Minnesota,2706,congressional_district/MN-06 +congressional_district/MN-07,Minnesota's 7th congressional district,congressional_district,state/mn,MN,Minnesota,2707,congressional_district/MN-07 +congressional_district/MN-08,Minnesota's 8th congressional district,congressional_district,state/mn,MN,Minnesota,2708,congressional_district/MN-08 +congressional_district/MO-01,Missouri's 1st congressional district,congressional_district,state/mo,MO,Missouri,2901,congressional_district/MO-01 +congressional_district/MO-02,Missouri's 2nd congressional district,congressional_district,state/mo,MO,Missouri,2902,congressional_district/MO-02 +congressional_district/MO-03,Missouri's 3rd congressional district,congressional_district,state/mo,MO,Missouri,2903,congressional_district/MO-03 +congressional_district/MO-04,Missouri's 4th congressional district,congressional_district,state/mo,MO,Missouri,2904,congressional_district/MO-04 +congressional_district/MO-05,Missouri's 5th congressional district,congressional_district,state/mo,MO,Missouri,2905,congressional_district/MO-05 +congressional_district/MO-06,Missouri's 6th congressional district,congressional_district,state/mo,MO,Missouri,2906,congressional_district/MO-06 +congressional_district/MO-07,Missouri's 7th congressional district,congressional_district,state/mo,MO,Missouri,2907,congressional_district/MO-07 +congressional_district/MO-08,Missouri's 8th congressional district,congressional_district,state/mo,MO,Missouri,2908,congressional_district/MO-08 +congressional_district/MS-01,Mississippi's 1st congressional district,congressional_district,state/ms,MS,Mississippi,2801,congressional_district/MS-01 +congressional_district/MS-02,Mississippi's 2nd congressional district,congressional_district,state/ms,MS,Mississippi,2802,congressional_district/MS-02 +congressional_district/MS-03,Mississippi's 3rd congressional district,congressional_district,state/ms,MS,Mississippi,2803,congressional_district/MS-03 +congressional_district/MS-04,Mississippi's 4th congressional district,congressional_district,state/ms,MS,Mississippi,2804,congressional_district/MS-04 +congressional_district/MT-01,Montana's 1st congressional district,congressional_district,state/mt,MT,Montana,3001,congressional_district/MT-01 +congressional_district/MT-02,Montana's 2nd congressional district,congressional_district,state/mt,MT,Montana,3002,congressional_district/MT-02 +congressional_district/NC-01,North Carolina's 1st congressional district,congressional_district,state/nc,NC,North Carolina,3701,congressional_district/NC-01 +congressional_district/NC-02,North Carolina's 2nd congressional district,congressional_district,state/nc,NC,North Carolina,3702,congressional_district/NC-02 +congressional_district/NC-03,North Carolina's 3rd congressional district,congressional_district,state/nc,NC,North Carolina,3703,congressional_district/NC-03 +congressional_district/NC-04,North Carolina's 4th congressional district,congressional_district,state/nc,NC,North Carolina,3704,congressional_district/NC-04 +congressional_district/NC-05,North Carolina's 5th congressional district,congressional_district,state/nc,NC,North Carolina,3705,congressional_district/NC-05 +congressional_district/NC-06,North Carolina's 6th congressional district,congressional_district,state/nc,NC,North Carolina,3706,congressional_district/NC-06 +congressional_district/NC-07,North Carolina's 7th congressional district,congressional_district,state/nc,NC,North Carolina,3707,congressional_district/NC-07 +congressional_district/NC-08,North Carolina's 8th congressional district,congressional_district,state/nc,NC,North Carolina,3708,congressional_district/NC-08 +congressional_district/NC-09,North Carolina's 9th congressional district,congressional_district,state/nc,NC,North Carolina,3709,congressional_district/NC-09 +congressional_district/NC-10,North Carolina's 10th congressional district,congressional_district,state/nc,NC,North Carolina,3710,congressional_district/NC-10 +congressional_district/NC-11,North Carolina's 11th congressional district,congressional_district,state/nc,NC,North Carolina,3711,congressional_district/NC-11 +congressional_district/NC-12,North Carolina's 12th congressional district,congressional_district,state/nc,NC,North Carolina,3712,congressional_district/NC-12 +congressional_district/NC-13,North Carolina's 13th congressional district,congressional_district,state/nc,NC,North Carolina,3713,congressional_district/NC-13 +congressional_district/NC-14,North Carolina's 14th congressional district,congressional_district,state/nc,NC,North Carolina,3714,congressional_district/NC-14 +congressional_district/ND-01,North Dakota's at-large congressional district,congressional_district,state/nd,ND,North Dakota,3800,congressional_district/ND-01 +congressional_district/NE-01,Nebraska's 1st congressional district,congressional_district,state/ne,NE,Nebraska,3101,congressional_district/NE-01 +congressional_district/NE-02,Nebraska's 2nd congressional district,congressional_district,state/ne,NE,Nebraska,3102,congressional_district/NE-02 +congressional_district/NE-03,Nebraska's 3rd congressional district,congressional_district,state/ne,NE,Nebraska,3103,congressional_district/NE-03 +congressional_district/NH-01,New Hampshire's 1st congressional district,congressional_district,state/nh,NH,New Hampshire,3301,congressional_district/NH-01 +congressional_district/NH-02,New Hampshire's 2nd congressional district,congressional_district,state/nh,NH,New Hampshire,3302,congressional_district/NH-02 +congressional_district/NJ-01,New Jersey's 1st congressional district,congressional_district,state/nj,NJ,New Jersey,3401,congressional_district/NJ-01 +congressional_district/NJ-02,New Jersey's 2nd congressional district,congressional_district,state/nj,NJ,New Jersey,3402,congressional_district/NJ-02 +congressional_district/NJ-03,New Jersey's 3rd congressional district,congressional_district,state/nj,NJ,New Jersey,3403,congressional_district/NJ-03 +congressional_district/NJ-04,New Jersey's 4th congressional district,congressional_district,state/nj,NJ,New Jersey,3404,congressional_district/NJ-04 +congressional_district/NJ-05,New Jersey's 5th congressional district,congressional_district,state/nj,NJ,New Jersey,3405,congressional_district/NJ-05 +congressional_district/NJ-06,New Jersey's 6th congressional district,congressional_district,state/nj,NJ,New Jersey,3406,congressional_district/NJ-06 +congressional_district/NJ-07,New Jersey's 7th congressional district,congressional_district,state/nj,NJ,New Jersey,3407,congressional_district/NJ-07 +congressional_district/NJ-08,New Jersey's 8th congressional district,congressional_district,state/nj,NJ,New Jersey,3408,congressional_district/NJ-08 +congressional_district/NJ-09,New Jersey's 9th congressional district,congressional_district,state/nj,NJ,New Jersey,3409,congressional_district/NJ-09 +congressional_district/NJ-10,New Jersey's 10th congressional district,congressional_district,state/nj,NJ,New Jersey,3410,congressional_district/NJ-10 +congressional_district/NJ-11,New Jersey's 11th congressional district,congressional_district,state/nj,NJ,New Jersey,3411,congressional_district/NJ-11 +congressional_district/NJ-12,New Jersey's 12th congressional district,congressional_district,state/nj,NJ,New Jersey,3412,congressional_district/NJ-12 +congressional_district/NM-01,New Mexico's 1st congressional district,congressional_district,state/nm,NM,New Mexico,3501,congressional_district/NM-01 +congressional_district/NM-02,New Mexico's 2nd congressional district,congressional_district,state/nm,NM,New Mexico,3502,congressional_district/NM-02 +congressional_district/NM-03,New Mexico's 3rd congressional district,congressional_district,state/nm,NM,New Mexico,3503,congressional_district/NM-03 +congressional_district/NV-01,Nevada's 1st congressional district,congressional_district,state/nv,NV,Nevada,3201,congressional_district/NV-01 +congressional_district/NV-02,Nevada's 2nd congressional district,congressional_district,state/nv,NV,Nevada,3202,congressional_district/NV-02 +congressional_district/NV-03,Nevada's 3rd congressional district,congressional_district,state/nv,NV,Nevada,3203,congressional_district/NV-03 +congressional_district/NV-04,Nevada's 4th congressional district,congressional_district,state/nv,NV,Nevada,3204,congressional_district/NV-04 +congressional_district/NY-01,New York's 1st congressional district,congressional_district,state/ny,NY,New York,3601,congressional_district/NY-01 +congressional_district/NY-02,New York's 2nd congressional district,congressional_district,state/ny,NY,New York,3602,congressional_district/NY-02 +congressional_district/NY-03,New York's 3rd congressional district,congressional_district,state/ny,NY,New York,3603,congressional_district/NY-03 +congressional_district/NY-04,New York's 4th congressional district,congressional_district,state/ny,NY,New York,3604,congressional_district/NY-04 +congressional_district/NY-05,New York's 5th congressional district,congressional_district,state/ny,NY,New York,3605,congressional_district/NY-05 +congressional_district/NY-06,New York's 6th congressional district,congressional_district,state/ny,NY,New York,3606,congressional_district/NY-06 +congressional_district/NY-07,New York's 7th congressional district,congressional_district,state/ny,NY,New York,3607,congressional_district/NY-07 +congressional_district/NY-08,New York's 8th congressional district,congressional_district,state/ny,NY,New York,3608,congressional_district/NY-08 +congressional_district/NY-09,New York's 9th congressional district,congressional_district,state/ny,NY,New York,3609,congressional_district/NY-09 +congressional_district/NY-10,New York's 10th congressional district,congressional_district,state/ny,NY,New York,3610,congressional_district/NY-10 +congressional_district/NY-11,New York's 11th congressional district,congressional_district,state/ny,NY,New York,3611,congressional_district/NY-11 +congressional_district/NY-12,New York's 12th congressional district,congressional_district,state/ny,NY,New York,3612,congressional_district/NY-12 +congressional_district/NY-13,New York's 13th congressional district,congressional_district,state/ny,NY,New York,3613,congressional_district/NY-13 +congressional_district/NY-14,New York's 14th congressional district,congressional_district,state/ny,NY,New York,3614,congressional_district/NY-14 +congressional_district/NY-15,New York's 15th congressional district,congressional_district,state/ny,NY,New York,3615,congressional_district/NY-15 +congressional_district/NY-16,New York's 16th congressional district,congressional_district,state/ny,NY,New York,3616,congressional_district/NY-16 +congressional_district/NY-17,New York's 17th congressional district,congressional_district,state/ny,NY,New York,3617,congressional_district/NY-17 +congressional_district/NY-18,New York's 18th congressional district,congressional_district,state/ny,NY,New York,3618,congressional_district/NY-18 +congressional_district/NY-19,New York's 19th congressional district,congressional_district,state/ny,NY,New York,3619,congressional_district/NY-19 +congressional_district/NY-20,New York's 20th congressional district,congressional_district,state/ny,NY,New York,3620,congressional_district/NY-20 +congressional_district/NY-21,New York's 21st congressional district,congressional_district,state/ny,NY,New York,3621,congressional_district/NY-21 +congressional_district/NY-22,New York's 22nd congressional district,congressional_district,state/ny,NY,New York,3622,congressional_district/NY-22 +congressional_district/NY-23,New York's 23rd congressional district,congressional_district,state/ny,NY,New York,3623,congressional_district/NY-23 +congressional_district/NY-24,New York's 24th congressional district,congressional_district,state/ny,NY,New York,3624,congressional_district/NY-24 +congressional_district/NY-25,New York's 25th congressional district,congressional_district,state/ny,NY,New York,3625,congressional_district/NY-25 +congressional_district/NY-26,New York's 26th congressional district,congressional_district,state/ny,NY,New York,3626,congressional_district/NY-26 +congressional_district/OH-01,Ohio's 1st congressional district,congressional_district,state/oh,OH,Ohio,3901,congressional_district/OH-01 +congressional_district/OH-02,Ohio's 2nd congressional district,congressional_district,state/oh,OH,Ohio,3902,congressional_district/OH-02 +congressional_district/OH-03,Ohio's 3rd congressional district,congressional_district,state/oh,OH,Ohio,3903,congressional_district/OH-03 +congressional_district/OH-04,Ohio's 4th congressional district,congressional_district,state/oh,OH,Ohio,3904,congressional_district/OH-04 +congressional_district/OH-05,Ohio's 5th congressional district,congressional_district,state/oh,OH,Ohio,3905,congressional_district/OH-05 +congressional_district/OH-06,Ohio's 6th congressional district,congressional_district,state/oh,OH,Ohio,3906,congressional_district/OH-06 +congressional_district/OH-07,Ohio's 7th congressional district,congressional_district,state/oh,OH,Ohio,3907,congressional_district/OH-07 +congressional_district/OH-08,Ohio's 8th congressional district,congressional_district,state/oh,OH,Ohio,3908,congressional_district/OH-08 +congressional_district/OH-09,Ohio's 9th congressional district,congressional_district,state/oh,OH,Ohio,3909,congressional_district/OH-09 +congressional_district/OH-10,Ohio's 10th congressional district,congressional_district,state/oh,OH,Ohio,3910,congressional_district/OH-10 +congressional_district/OH-11,Ohio's 11th congressional district,congressional_district,state/oh,OH,Ohio,3911,congressional_district/OH-11 +congressional_district/OH-12,Ohio's 12th congressional district,congressional_district,state/oh,OH,Ohio,3912,congressional_district/OH-12 +congressional_district/OH-13,Ohio's 13th congressional district,congressional_district,state/oh,OH,Ohio,3913,congressional_district/OH-13 +congressional_district/OH-14,Ohio's 14th congressional district,congressional_district,state/oh,OH,Ohio,3914,congressional_district/OH-14 +congressional_district/OH-15,Ohio's 15th congressional district,congressional_district,state/oh,OH,Ohio,3915,congressional_district/OH-15 +congressional_district/OK-01,Oklahoma's 1st congressional district,congressional_district,state/ok,OK,Oklahoma,4001,congressional_district/OK-01 +congressional_district/OK-02,Oklahoma's 2nd congressional district,congressional_district,state/ok,OK,Oklahoma,4002,congressional_district/OK-02 +congressional_district/OK-03,Oklahoma's 3rd congressional district,congressional_district,state/ok,OK,Oklahoma,4003,congressional_district/OK-03 +congressional_district/OK-04,Oklahoma's 4th congressional district,congressional_district,state/ok,OK,Oklahoma,4004,congressional_district/OK-04 +congressional_district/OK-05,Oklahoma's 5th congressional district,congressional_district,state/ok,OK,Oklahoma,4005,congressional_district/OK-05 +congressional_district/OR-01,Oregon's 1st congressional district,congressional_district,state/or,OR,Oregon,4101,congressional_district/OR-01 +congressional_district/OR-02,Oregon's 2nd congressional district,congressional_district,state/or,OR,Oregon,4102,congressional_district/OR-02 +congressional_district/OR-03,Oregon's 3rd congressional district,congressional_district,state/or,OR,Oregon,4103,congressional_district/OR-03 +congressional_district/OR-04,Oregon's 4th congressional district,congressional_district,state/or,OR,Oregon,4104,congressional_district/OR-04 +congressional_district/OR-05,Oregon's 5th congressional district,congressional_district,state/or,OR,Oregon,4105,congressional_district/OR-05 +congressional_district/OR-06,Oregon's 6th congressional district,congressional_district,state/or,OR,Oregon,4106,congressional_district/OR-06 +congressional_district/PA-01,Pennsylvania's 1st congressional district,congressional_district,state/pa,PA,Pennsylvania,4201,congressional_district/PA-01 +congressional_district/PA-02,Pennsylvania's 2nd congressional district,congressional_district,state/pa,PA,Pennsylvania,4202,congressional_district/PA-02 +congressional_district/PA-03,Pennsylvania's 3rd congressional district,congressional_district,state/pa,PA,Pennsylvania,4203,congressional_district/PA-03 +congressional_district/PA-04,Pennsylvania's 4th congressional district,congressional_district,state/pa,PA,Pennsylvania,4204,congressional_district/PA-04 +congressional_district/PA-05,Pennsylvania's 5th congressional district,congressional_district,state/pa,PA,Pennsylvania,4205,congressional_district/PA-05 +congressional_district/PA-06,Pennsylvania's 6th congressional district,congressional_district,state/pa,PA,Pennsylvania,4206,congressional_district/PA-06 +congressional_district/PA-07,Pennsylvania's 7th congressional district,congressional_district,state/pa,PA,Pennsylvania,4207,congressional_district/PA-07 +congressional_district/PA-08,Pennsylvania's 8th congressional district,congressional_district,state/pa,PA,Pennsylvania,4208,congressional_district/PA-08 +congressional_district/PA-09,Pennsylvania's 9th congressional district,congressional_district,state/pa,PA,Pennsylvania,4209,congressional_district/PA-09 +congressional_district/PA-10,Pennsylvania's 10th congressional district,congressional_district,state/pa,PA,Pennsylvania,4210,congressional_district/PA-10 +congressional_district/PA-11,Pennsylvania's 11th congressional district,congressional_district,state/pa,PA,Pennsylvania,4211,congressional_district/PA-11 +congressional_district/PA-12,Pennsylvania's 12th congressional district,congressional_district,state/pa,PA,Pennsylvania,4212,congressional_district/PA-12 +congressional_district/PA-13,Pennsylvania's 13th congressional district,congressional_district,state/pa,PA,Pennsylvania,4213,congressional_district/PA-13 +congressional_district/PA-14,Pennsylvania's 14th congressional district,congressional_district,state/pa,PA,Pennsylvania,4214,congressional_district/PA-14 +congressional_district/PA-15,Pennsylvania's 15th congressional district,congressional_district,state/pa,PA,Pennsylvania,4215,congressional_district/PA-15 +congressional_district/PA-16,Pennsylvania's 16th congressional district,congressional_district,state/pa,PA,Pennsylvania,4216,congressional_district/PA-16 +congressional_district/PA-17,Pennsylvania's 17th congressional district,congressional_district,state/pa,PA,Pennsylvania,4217,congressional_district/PA-17 +congressional_district/RI-01,Rhode Island's 1st congressional district,congressional_district,state/ri,RI,Rhode Island,4401,congressional_district/RI-01 +congressional_district/RI-02,Rhode Island's 2nd congressional district,congressional_district,state/ri,RI,Rhode Island,4402,congressional_district/RI-02 +congressional_district/SC-01,South Carolina's 1st congressional district,congressional_district,state/sc,SC,South Carolina,4501,congressional_district/SC-01 +congressional_district/SC-02,South Carolina's 2nd congressional district,congressional_district,state/sc,SC,South Carolina,4502,congressional_district/SC-02 +congressional_district/SC-03,South Carolina's 3rd congressional district,congressional_district,state/sc,SC,South Carolina,4503,congressional_district/SC-03 +congressional_district/SC-04,South Carolina's 4th congressional district,congressional_district,state/sc,SC,South Carolina,4504,congressional_district/SC-04 +congressional_district/SC-05,South Carolina's 5th congressional district,congressional_district,state/sc,SC,South Carolina,4505,congressional_district/SC-05 +congressional_district/SC-06,South Carolina's 6th congressional district,congressional_district,state/sc,SC,South Carolina,4506,congressional_district/SC-06 +congressional_district/SC-07,South Carolina's 7th congressional district,congressional_district,state/sc,SC,South Carolina,4507,congressional_district/SC-07 +congressional_district/SD-01,South Dakota's at-large congressional district,congressional_district,state/sd,SD,South Dakota,4600,congressional_district/SD-01 +congressional_district/TN-01,Tennessee's 1st congressional district,congressional_district,state/tn,TN,Tennessee,4701,congressional_district/TN-01 +congressional_district/TN-02,Tennessee's 2nd congressional district,congressional_district,state/tn,TN,Tennessee,4702,congressional_district/TN-02 +congressional_district/TN-03,Tennessee's 3rd congressional district,congressional_district,state/tn,TN,Tennessee,4703,congressional_district/TN-03 +congressional_district/TN-04,Tennessee's 4th congressional district,congressional_district,state/tn,TN,Tennessee,4704,congressional_district/TN-04 +congressional_district/TN-05,Tennessee's 5th congressional district,congressional_district,state/tn,TN,Tennessee,4705,congressional_district/TN-05 +congressional_district/TN-06,Tennessee's 6th congressional district,congressional_district,state/tn,TN,Tennessee,4706,congressional_district/TN-06 +congressional_district/TN-07,Tennessee's 7th congressional district,congressional_district,state/tn,TN,Tennessee,4707,congressional_district/TN-07 +congressional_district/TN-08,Tennessee's 8th congressional district,congressional_district,state/tn,TN,Tennessee,4708,congressional_district/TN-08 +congressional_district/TN-09,Tennessee's 9th congressional district,congressional_district,state/tn,TN,Tennessee,4709,congressional_district/TN-09 +congressional_district/TX-01,Texas's 1st congressional district,congressional_district,state/tx,TX,Texas,4801,congressional_district/TX-01 +congressional_district/TX-02,Texas's 2nd congressional district,congressional_district,state/tx,TX,Texas,4802,congressional_district/TX-02 +congressional_district/TX-03,Texas's 3rd congressional district,congressional_district,state/tx,TX,Texas,4803,congressional_district/TX-03 +congressional_district/TX-04,Texas's 4th congressional district,congressional_district,state/tx,TX,Texas,4804,congressional_district/TX-04 +congressional_district/TX-05,Texas's 5th congressional district,congressional_district,state/tx,TX,Texas,4805,congressional_district/TX-05 +congressional_district/TX-06,Texas's 6th congressional district,congressional_district,state/tx,TX,Texas,4806,congressional_district/TX-06 +congressional_district/TX-07,Texas's 7th congressional district,congressional_district,state/tx,TX,Texas,4807,congressional_district/TX-07 +congressional_district/TX-08,Texas's 8th congressional district,congressional_district,state/tx,TX,Texas,4808,congressional_district/TX-08 +congressional_district/TX-09,Texas's 9th congressional district,congressional_district,state/tx,TX,Texas,4809,congressional_district/TX-09 +congressional_district/TX-10,Texas's 10th congressional district,congressional_district,state/tx,TX,Texas,4810,congressional_district/TX-10 +congressional_district/TX-11,Texas's 11th congressional district,congressional_district,state/tx,TX,Texas,4811,congressional_district/TX-11 +congressional_district/TX-12,Texas's 12th congressional district,congressional_district,state/tx,TX,Texas,4812,congressional_district/TX-12 +congressional_district/TX-13,Texas's 13th congressional district,congressional_district,state/tx,TX,Texas,4813,congressional_district/TX-13 +congressional_district/TX-14,Texas's 14th congressional district,congressional_district,state/tx,TX,Texas,4814,congressional_district/TX-14 +congressional_district/TX-15,Texas's 15th congressional district,congressional_district,state/tx,TX,Texas,4815,congressional_district/TX-15 +congressional_district/TX-16,Texas's 16th congressional district,congressional_district,state/tx,TX,Texas,4816,congressional_district/TX-16 +congressional_district/TX-17,Texas's 17th congressional district,congressional_district,state/tx,TX,Texas,4817,congressional_district/TX-17 +congressional_district/TX-18,Texas's 18th congressional district,congressional_district,state/tx,TX,Texas,4818,congressional_district/TX-18 +congressional_district/TX-19,Texas's 19th congressional district,congressional_district,state/tx,TX,Texas,4819,congressional_district/TX-19 +congressional_district/TX-20,Texas's 20th congressional district,congressional_district,state/tx,TX,Texas,4820,congressional_district/TX-20 +congressional_district/TX-21,Texas's 21st congressional district,congressional_district,state/tx,TX,Texas,4821,congressional_district/TX-21 +congressional_district/TX-22,Texas's 22nd congressional district,congressional_district,state/tx,TX,Texas,4822,congressional_district/TX-22 +congressional_district/TX-23,Texas's 23rd congressional district,congressional_district,state/tx,TX,Texas,4823,congressional_district/TX-23 +congressional_district/TX-24,Texas's 24th congressional district,congressional_district,state/tx,TX,Texas,4824,congressional_district/TX-24 +congressional_district/TX-25,Texas's 25th congressional district,congressional_district,state/tx,TX,Texas,4825,congressional_district/TX-25 +congressional_district/TX-26,Texas's 26th congressional district,congressional_district,state/tx,TX,Texas,4826,congressional_district/TX-26 +congressional_district/TX-27,Texas's 27th congressional district,congressional_district,state/tx,TX,Texas,4827,congressional_district/TX-27 +congressional_district/TX-28,Texas's 28th congressional district,congressional_district,state/tx,TX,Texas,4828,congressional_district/TX-28 +congressional_district/TX-29,Texas's 29th congressional district,congressional_district,state/tx,TX,Texas,4829,congressional_district/TX-29 +congressional_district/TX-30,Texas's 30th congressional district,congressional_district,state/tx,TX,Texas,4830,congressional_district/TX-30 +congressional_district/TX-31,Texas's 31st congressional district,congressional_district,state/tx,TX,Texas,4831,congressional_district/TX-31 +congressional_district/TX-32,Texas's 32nd congressional district,congressional_district,state/tx,TX,Texas,4832,congressional_district/TX-32 +congressional_district/TX-33,Texas's 33rd congressional district,congressional_district,state/tx,TX,Texas,4833,congressional_district/TX-33 +congressional_district/TX-34,Texas's 34th congressional district,congressional_district,state/tx,TX,Texas,4834,congressional_district/TX-34 +congressional_district/TX-35,Texas's 35th congressional district,congressional_district,state/tx,TX,Texas,4835,congressional_district/TX-35 +congressional_district/TX-36,Texas's 36th congressional district,congressional_district,state/tx,TX,Texas,4836,congressional_district/TX-36 +congressional_district/TX-37,Texas's 37th congressional district,congressional_district,state/tx,TX,Texas,4837,congressional_district/TX-37 +congressional_district/TX-38,Texas's 38th congressional district,congressional_district,state/tx,TX,Texas,4838,congressional_district/TX-38 +congressional_district/UT-01,Utah's 1st congressional district,congressional_district,state/ut,UT,Utah,4901,congressional_district/UT-01 +congressional_district/UT-02,Utah's 2nd congressional district,congressional_district,state/ut,UT,Utah,4902,congressional_district/UT-02 +congressional_district/UT-03,Utah's 3rd congressional district,congressional_district,state/ut,UT,Utah,4903,congressional_district/UT-03 +congressional_district/UT-04,Utah's 4th congressional district,congressional_district,state/ut,UT,Utah,4904,congressional_district/UT-04 +congressional_district/VA-01,Virginia's 1st congressional district,congressional_district,state/va,VA,Virginia,5101,congressional_district/VA-01 +congressional_district/VA-02,Virginia's 2nd congressional district,congressional_district,state/va,VA,Virginia,5102,congressional_district/VA-02 +congressional_district/VA-03,Virginia's 3rd congressional district,congressional_district,state/va,VA,Virginia,5103,congressional_district/VA-03 +congressional_district/VA-04,Virginia's 4th congressional district,congressional_district,state/va,VA,Virginia,5104,congressional_district/VA-04 +congressional_district/VA-05,Virginia's 5th congressional district,congressional_district,state/va,VA,Virginia,5105,congressional_district/VA-05 +congressional_district/VA-06,Virginia's 6th congressional district,congressional_district,state/va,VA,Virginia,5106,congressional_district/VA-06 +congressional_district/VA-07,Virginia's 7th congressional district,congressional_district,state/va,VA,Virginia,5107,congressional_district/VA-07 +congressional_district/VA-08,Virginia's 8th congressional district,congressional_district,state/va,VA,Virginia,5108,congressional_district/VA-08 +congressional_district/VA-09,Virginia's 9th congressional district,congressional_district,state/va,VA,Virginia,5109,congressional_district/VA-09 +congressional_district/VA-10,Virginia's 10th congressional district,congressional_district,state/va,VA,Virginia,5110,congressional_district/VA-10 +congressional_district/VA-11,Virginia's 11th congressional district,congressional_district,state/va,VA,Virginia,5111,congressional_district/VA-11 +congressional_district/VT-01,Vermont's at-large congressional district,congressional_district,state/vt,VT,Vermont,5000,congressional_district/VT-01 +congressional_district/WA-01,Washington's 1st congressional district,congressional_district,state/wa,WA,Washington,5301,congressional_district/WA-01 +congressional_district/WA-02,Washington's 2nd congressional district,congressional_district,state/wa,WA,Washington,5302,congressional_district/WA-02 +congressional_district/WA-03,Washington's 3rd congressional district,congressional_district,state/wa,WA,Washington,5303,congressional_district/WA-03 +congressional_district/WA-04,Washington's 4th congressional district,congressional_district,state/wa,WA,Washington,5304,congressional_district/WA-04 +congressional_district/WA-05,Washington's 5th congressional district,congressional_district,state/wa,WA,Washington,5305,congressional_district/WA-05 +congressional_district/WA-06,Washington's 6th congressional district,congressional_district,state/wa,WA,Washington,5306,congressional_district/WA-06 +congressional_district/WA-07,Washington's 7th congressional district,congressional_district,state/wa,WA,Washington,5307,congressional_district/WA-07 +congressional_district/WA-08,Washington's 8th congressional district,congressional_district,state/wa,WA,Washington,5308,congressional_district/WA-08 +congressional_district/WA-09,Washington's 9th congressional district,congressional_district,state/wa,WA,Washington,5309,congressional_district/WA-09 +congressional_district/WA-10,Washington's 10th congressional district,congressional_district,state/wa,WA,Washington,5310,congressional_district/WA-10 +congressional_district/WI-01,Wisconsin's 1st congressional district,congressional_district,state/wi,WI,Wisconsin,5501,congressional_district/WI-01 +congressional_district/WI-02,Wisconsin's 2nd congressional district,congressional_district,state/wi,WI,Wisconsin,5502,congressional_district/WI-02 +congressional_district/WI-03,Wisconsin's 3rd congressional district,congressional_district,state/wi,WI,Wisconsin,5503,congressional_district/WI-03 +congressional_district/WI-04,Wisconsin's 4th congressional district,congressional_district,state/wi,WI,Wisconsin,5504,congressional_district/WI-04 +congressional_district/WI-05,Wisconsin's 5th congressional district,congressional_district,state/wi,WI,Wisconsin,5505,congressional_district/WI-05 +congressional_district/WI-06,Wisconsin's 6th congressional district,congressional_district,state/wi,WI,Wisconsin,5506,congressional_district/WI-06 +congressional_district/WI-07,Wisconsin's 7th congressional district,congressional_district,state/wi,WI,Wisconsin,5507,congressional_district/WI-07 +congressional_district/WI-08,Wisconsin's 8th congressional district,congressional_district,state/wi,WI,Wisconsin,5508,congressional_district/WI-08 +congressional_district/WV-01,West Virginia's 1st congressional district,congressional_district,state/wv,WV,West Virginia,5401,congressional_district/WV-01 +congressional_district/WV-02,West Virginia's 2nd congressional district,congressional_district,state/wv,WV,West Virginia,5402,congressional_district/WV-02 +congressional_district/WY-01,Wyoming's at-large congressional district,congressional_district,state/wy,WY,Wyoming,5600,congressional_district/WY-01 +place/AK-03000,Anchorage,place,state/ak,AK,Alaska,,place/AK-03000 +place/AL-07000,Birmingham,place,state/al,AL,Alabama,,place/AL-07000 +place/AL-37000,Huntsville,place,state/al,AL,Alabama,,place/AL-37000 +place/AL-50000,Mobile,place,state/al,AL,Alabama,,place/AL-50000 +place/AL-51000,Montgomery,place,state/al,AL,Alabama,,place/AL-51000 +place/AL-77256,Tuscaloosa,place,state/al,AL,Alabama,,place/AL-77256 +place/AR-23290,Fayetteville,place,state/ar,AR,Arkansas,,place/AR-23290 +place/AR-41000,Little Rock,place,state/ar,AR,Arkansas,,place/AR-41000 +place/AZ-07940,Buckeye,place,state/az,AZ,Arizona,,place/AZ-07940 +place/AZ-12000,Chandler,place,state/az,AZ,Arizona,,place/AZ-12000 +place/AZ-27400,Gilbert,place,state/az,AZ,Arizona,,place/AZ-27400 +place/AZ-27820,Glendale,place,state/az,AZ,Arizona,,place/AZ-27820 +place/AZ-28380,Goodyear,place,state/az,AZ,Arizona,,place/AZ-28380 +place/AZ-46000,Mesa,place,state/az,AZ,Arizona,,place/AZ-46000 +place/AZ-54050,Peoria,place,state/az,AZ,Arizona,,place/AZ-54050 +place/AZ-55000,Phoenix,place,state/az,AZ,Arizona,,place/AZ-55000 +place/AZ-65000,Scottsdale,place,state/az,AZ,Arizona,,place/AZ-65000 +place/AZ-71510,Surprise,place,state/az,AZ,Arizona,,place/AZ-71510 +place/AZ-73000,Tempe,place,state/az,AZ,Arizona,,place/AZ-73000 +place/AZ-77000,Tucson,place,state/az,AZ,Arizona,,place/AZ-77000 +place/AZ-85540,Yuma,place,state/az,AZ,Arizona,,place/AZ-85540 +place/CA-02000,Anaheim,place,state/ca,CA,California,,place/CA-02000 +place/CA-02252,Antioch,place,state/ca,CA,California,,place/CA-02252 +place/CA-03526,Bakersfield,place,state/ca,CA,California,,place/CA-03526 +place/CA-06000,Berkeley,place,state/ca,CA,California,,place/CA-06000 +place/CA-08954,Burbank,place,state/ca,CA,California,,place/CA-08954 +place/CA-11194,Carlsbad,place,state/ca,CA,California,,place/CA-11194 +place/CA-13014,Chico,place,state/ca,CA,California,,place/CA-13014 +place/CA-13392,Chula Vista,place,state/ca,CA,California,,place/CA-13392 +place/CA-14218,Clovis,place,state/ca,CA,California,,place/CA-14218 +place/CA-16000,Concord,place,state/ca,CA,California,,place/CA-16000 +place/CA-16350,Corona,place,state/ca,CA,California,,place/CA-16350 +place/CA-16532,Costa Mesa,place,state/ca,CA,California,,place/CA-16532 +place/CA-19766,Downey,place,state/ca,CA,California,,place/CA-19766 +place/CA-21712,El Cajon,place,state/ca,CA,California,,place/CA-21712 +place/CA-22020,Elk Grove,place,state/ca,CA,California,,place/CA-22020 +place/CA-22230,El Monte,place,state/ca,CA,California,,place/CA-22230 +place/CA-22804,Escondido,place,state/ca,CA,California,,place/CA-22804 +place/CA-23182,Fairfield,place,state/ca,CA,California,,place/CA-23182 +place/CA-24680,Fontana,place,state/ca,CA,California,,place/CA-24680 +place/CA-26000,Fremont,place,state/ca,CA,California,,place/CA-26000 +place/CA-27000,Fresno,place,state/ca,CA,California,,place/CA-27000 +place/CA-28000,Fullerton,place,state/ca,CA,California,,place/CA-28000 +place/CA-29000,Garden Grove,place,state/ca,CA,California,,place/CA-29000 +place/CA-30000,Glendale,place,state/ca,CA,California,,place/CA-30000 +place/CA-33000,Hayward,place,state/ca,CA,California,,place/CA-33000 +place/CA-33434,Hesperia,place,state/ca,CA,California,,place/CA-33434 +place/CA-36000,Huntington Beach,place,state/ca,CA,California,,place/CA-36000 +place/CA-36546,Inglewood,place,state/ca,CA,California,,place/CA-36546 +place/CA-36770,Irvine,place,state/ca,CA,California,,place/CA-36770 +place/CA-37692,Jurupa Valley,place,state/ca,CA,California,,place/CA-37692 +place/CA-40130,Lancaster,place,state/ca,CA,California,,place/CA-40130 +place/CA-43000,Long Beach,place,state/ca,CA,California,,place/CA-43000 +place/CA-44000,Los Angeles,place,state/ca,CA,California,,place/CA-44000 +place/CA-46842,Menifee,place,state/ca,CA,California,,place/CA-46842 +place/CA-48354,Modesto,place,state/ca,CA,California,,place/CA-48354 +place/CA-49270,Moreno Valley,place,state/ca,CA,California,,place/CA-49270 +place/CA-50076,Murrieta,place,state/ca,CA,California,,place/CA-50076 +place/CA-53000,Oakland,place,state/ca,CA,California,,place/CA-53000 +place/CA-53322,Oceanside,place,state/ca,CA,California,,place/CA-53322 +place/CA-53896,Ontario,place,state/ca,CA,California,,place/CA-53896 +place/CA-53980,Orange,place,state/ca,CA,California,,place/CA-53980 +place/CA-54652,Oxnard,place,state/ca,CA,California,,place/CA-54652 +place/CA-55156,Palmdale,place,state/ca,CA,California,,place/CA-55156 +place/CA-56000,Pasadena,place,state/ca,CA,California,,place/CA-56000 +place/CA-58072,Pomona,place,state/ca,CA,California,,place/CA-58072 +place/CA-59451,Rancho Cucamonga,place,state/ca,CA,California,,place/CA-59451 +place/CA-60466,Rialto,place,state/ca,CA,California,,place/CA-60466 +place/CA-60620,Richmond,place,state/ca,CA,California,,place/CA-60620 +place/CA-62000,Riverside,place,state/ca,CA,California,,place/CA-62000 +place/CA-62938,Roseville,place,state/ca,CA,California,,place/CA-62938 +place/CA-64000,Sacramento,place,state/ca,CA,California,,place/CA-64000 +place/CA-64224,Salinas,place,state/ca,CA,California,,place/CA-64224 +place/CA-65000,San Bernardino,place,state/ca,CA,California,,place/CA-65000 +place/CA-65042,Ventura,place,state/ca,CA,California,,place/CA-65042 +place/CA-66000,San Diego,place,state/ca,CA,California,,place/CA-66000 +place/CA-67000,San Francisco,place,state/ca,CA,California,,place/CA-67000 +place/CA-68000,San Jose,place,state/ca,CA,California,,place/CA-68000 +place/CA-68252,San Mateo,place,state/ca,CA,California,,place/CA-68252 +place/CA-69000,Santa Ana,place,state/ca,CA,California,,place/CA-69000 +place/CA-69084,Santa Clara,place,state/ca,CA,California,,place/CA-69084 +place/CA-69088,Santa Clarita,place,state/ca,CA,California,,place/CA-69088 +place/CA-69196,Santa Maria,place,state/ca,CA,California,,place/CA-69196 +place/CA-70098,Santa Rosa,place,state/ca,CA,California,,place/CA-70098 +place/CA-72016,Simi Valley,place,state/ca,CA,California,,place/CA-72016 +place/CA-75000,Stockton,place,state/ca,CA,California,,place/CA-75000 +place/CA-77000,Sunnyvale,place,state/ca,CA,California,,place/CA-77000 +place/CA-78120,Temecula,place,state/ca,CA,California,,place/CA-78120 +place/CA-78582,Thousand Oaks,place,state/ca,CA,California,,place/CA-78582 +place/CA-80000,Torrance,place,state/ca,CA,California,,place/CA-80000 +place/CA-81554,Vacaville,place,state/ca,CA,California,,place/CA-81554 +place/CA-81666,Vallejo,place,state/ca,CA,California,,place/CA-81666 +place/CA-82590,Victorville,place,state/ca,CA,California,,place/CA-82590 +place/CA-82954,Visalia,place,state/ca,CA,California,,place/CA-82954 +place/CA-84200,West Covina,place,state/ca,CA,California,,place/CA-84200 +place/CO-03455,Arvada,place,state/co,CO,Colorado,,place/CO-03455 +place/CO-04000,Aurora,place,state/co,CO,Colorado,,place/CO-04000 +place/CO-07850,Boulder,place,state/co,CO,Colorado,,place/CO-07850 +place/CO-12815,Centennial,place,state/co,CO,Colorado,,place/CO-12815 +place/CO-16000,Colorado Springs,place,state/co,CO,Colorado,,place/CO-16000 +place/CO-20000,Denver,place,state/co,CO,Colorado,,place/CO-20000 +place/CO-27425,Fort Collins,place,state/co,CO,Colorado,,place/CO-27425 +place/CO-32155,Greeley,place,state/co,CO,Colorado,,place/CO-32155 +place/CO-43000,Lakewood,place,state/co,CO,Colorado,,place/CO-43000 +place/CO-62000,Pueblo,place,state/co,CO,Colorado,,place/CO-62000 +place/CO-77290,Thornton,place,state/co,CO,Colorado,,place/CO-77290 +place/CO-83835,Westminster,place,state/co,CO,Colorado,,place/CO-83835 +place/CT-08000,Bridgeport,place,state/ct,CT,Connecticut,,place/CT-08000 +place/CT-37000,Hartford,place,state/ct,CT,Connecticut,,place/CT-37000 +place/CT-52000,New Haven,place,state/ct,CT,Connecticut,,place/CT-52000 +place/CT-73000,Stamford,place,state/ct,CT,Connecticut,,place/CT-73000 +place/CT-80000,Waterbury,place,state/ct,CT,Connecticut,,place/CT-80000 +place/DC-50000,Washington,place,state/dc,DC,District of Columbia,,place/DC-50000 +place/FL-10275,Cape Coral,place,state/fl,FL,Florida,,place/FL-10275 +place/FL-12875,Clearwater,place,state/fl,FL,Florida,,place/FL-12875 +place/FL-14400,Coral Springs,place,state/fl,FL,Florida,,place/FL-14400 +place/FL-16475,Davie,place,state/fl,FL,Florida,,place/FL-16475 +place/FL-24000,Fort Lauderdale,place,state/fl,FL,Florida,,place/FL-24000 +place/FL-25175,Gainesville,place,state/fl,FL,Florida,,place/FL-25175 +place/FL-30000,Hialeah,place,state/fl,FL,Florida,,place/FL-30000 +place/FL-32000,Hollywood,place,state/fl,FL,Florida,,place/FL-32000 +place/FL-35000,Jacksonville,place,state/fl,FL,Florida,,place/FL-35000 +place/FL-38250,Lakeland,place,state/fl,FL,Florida,,place/FL-38250 +place/FL-45000,Miami,place,state/fl,FL,Florida,,place/FL-45000 +place/FL-45060,Miami Gardens,place,state/fl,FL,Florida,,place/FL-45060 +place/FL-45975,Miramar,place,state/fl,FL,Florida,,place/FL-45975 +place/FL-53000,Orlando,place,state/fl,FL,Florida,,place/FL-53000 +place/FL-54000,Palm Bay,place,state/fl,FL,Florida,,place/FL-54000 +place/FL-54200,Palm Coast,place,state/fl,FL,Florida,,place/FL-54200 +place/FL-55775,Pembroke Pines,place,state/fl,FL,Florida,,place/FL-55775 +place/FL-58050,Pompano Beach,place,state/fl,FL,Florida,,place/FL-58050 +place/FL-58715,Port St. Lucie,place,state/fl,FL,Florida,,place/FL-58715 +place/FL-63000,St. Petersburg,place,state/fl,FL,Florida,,place/FL-63000 +place/FL-70600,Tallahassee,place,state/fl,FL,Florida,,place/FL-70600 +place/FL-71000,Tampa,place,state/fl,FL,Florida,,place/FL-71000 +place/FL-76600,West Palm Beach,place,state/fl,FL,Florida,,place/FL-76600 +place/GA-03440,Athens-Clarke County,place,state/ga,GA,Georgia,,place/GA-03440 +place/GA-04000,Atlanta,place,state/ga,GA,Georgia,,place/GA-04000 +place/GA-04204,Augusta-Richmond County,place,state/ga,GA,Georgia,,place/GA-04204 +place/GA-19000,Columbus,place,state/ga,GA,Georgia,,place/GA-19000 +place/GA-49008,Macon-Bibb County,place,state/ga,GA,Georgia,,place/GA-49008 +place/GA-68516,Sandy Springs,place,state/ga,GA,Georgia,,place/GA-68516 +place/GA-69000,Savannah,place,state/ga,GA,Georgia,,place/GA-69000 +place/GA-72122,South Fulton,place,state/ga,GA,Georgia,,place/GA-72122 +place/HI-71550,Urban Honolulu,place,state/hi,HI,Hawaii,,place/HI-71550 +place/IA-12000,Cedar Rapids,place,state/ia,IA,Iowa,,place/IA-12000 +place/IA-19000,Davenport,place,state/ia,IA,Iowa,,place/IA-19000 +place/IA-21000,Des Moines,place,state/ia,IA,Iowa,,place/IA-21000 +place/ID-08830,Boise City,place,state/id,ID,Idaho,,place/ID-08830 +place/ID-52120,Meridian,place,state/id,ID,Idaho,,place/ID-52120 +place/ID-56260,Nampa,place,state/id,ID,Idaho,,place/ID-56260 +place/IL-03012,Aurora,place,state/il,IL,Illinois,,place/IL-03012 +place/IL-14000,Chicago,place,state/il,IL,Illinois,,place/IL-14000 +place/IL-23074,Elgin,place,state/il,IL,Illinois,,place/IL-23074 +place/IL-38570,Joliet,place,state/il,IL,Illinois,,place/IL-38570 +place/IL-51622,Naperville,place,state/il,IL,Illinois,,place/IL-51622 +place/IL-59000,Peoria,place,state/il,IL,Illinois,,place/IL-59000 +place/IL-65000,Rockford,place,state/il,IL,Illinois,,place/IL-65000 +place/IL-72000,Springfield,place,state/il,IL,Illinois,,place/IL-72000 +place/IN-10342,Carmel,place,state/in,IN,Indiana,,place/IN-10342 +place/IN-22000,Evansville,place,state/in,IN,Indiana,,place/IN-22000 +place/IN-23278,Fishers,place,state/in,IN,Indiana,,place/IN-23278 +place/IN-25000,Fort Wayne,place,state/in,IN,Indiana,,place/IN-25000 +place/IN-36003,Indianapolis,place,state/in,IN,Indiana,,place/IN-36003 +place/IN-71000,South Bend,place,state/in,IN,Indiana,,place/IN-71000 +place/KS-36000,Kansas City,place,state/ks,KS,Kansas,,place/KS-36000 +place/KS-52575,Olathe,place,state/ks,KS,Kansas,,place/KS-52575 +place/KS-53775,Overland Park,place,state/ks,KS,Kansas,,place/KS-53775 +place/KS-71000,Topeka,place,state/ks,KS,Kansas,,place/KS-71000 +place/KS-79000,Wichita,place,state/ks,KS,Kansas,,place/KS-79000 +place/KY-46027,Lexington-Fayette,place,state/ky,KY,Kentucky,,place/KY-46027 +place/KY-48006,Louisville/Jefferson County,place,state/ky,KY,Kentucky,,place/KY-48006 +place/LA-05000,Baton Rouge,place,state/la,LA,Louisiana,,place/LA-05000 +place/LA-40735,Lafayette,place,state/la,LA,Louisiana,,place/LA-40735 +place/LA-55000,New Orleans,place,state/la,LA,Louisiana,,place/LA-55000 +place/LA-70000,Shreveport,place,state/la,LA,Louisiana,,place/LA-70000 +place/MA-07000,Boston,place,state/ma,MA,Massachusetts,,place/MA-07000 +place/MA-09000,Brockton,place,state/ma,MA,Massachusetts,,place/MA-09000 +place/MA-11000,Cambridge,place,state/ma,MA,Massachusetts,,place/MA-11000 +place/MA-37000,Lowell,place,state/ma,MA,Massachusetts,,place/MA-37000 +place/MA-37490,Lynn,place,state/ma,MA,Massachusetts,,place/MA-37490 +place/MA-45000,New Bedford,place,state/ma,MA,Massachusetts,,place/MA-45000 +place/MA-55745,Quincy,place,state/ma,MA,Massachusetts,,place/MA-55745 +place/MA-67000,Springfield,place,state/ma,MA,Massachusetts,,place/MA-67000 +place/MA-82000,Worcester,place,state/ma,MA,Massachusetts,,place/MA-82000 +place/MD-04000,Baltimore,place,state/md,MD,Maryland,,place/MD-04000 +place/MI-03000,Ann Arbor,place,state/mi,MI,Michigan,,place/MI-03000 +place/MI-21000,Dearborn,place,state/mi,MI,Michigan,,place/MI-21000 +place/MI-22000,Detroit,place,state/mi,MI,Michigan,,place/MI-22000 +place/MI-34000,Grand Rapids,place,state/mi,MI,Michigan,,place/MI-34000 +place/MI-46000,Lansing,place,state/mi,MI,Michigan,,place/MI-46000 +place/MI-76460,Sterling Heights,place,state/mi,MI,Michigan,,place/MI-76460 +place/MI-84000,Warren,place,state/mi,MI,Michigan,,place/MI-84000 +place/MN-43000,Minneapolis,place,state/mn,MN,Minnesota,,place/MN-43000 +place/MN-54880,Rochester,place,state/mn,MN,Minnesota,,place/MN-54880 +place/MN-58000,St. Paul,place,state/mn,MN,Minnesota,,place/MN-58000 +place/MO-15670,Columbia,place,state/mo,MO,Missouri,,place/MO-15670 +place/MO-35000,Independence,place,state/mo,MO,Missouri,,place/MO-35000 +place/MO-38000,Kansas City,place,state/mo,MO,Missouri,,place/MO-38000 +place/MO-41348,Lee's Summit,place,state/mo,MO,Missouri,,place/MO-41348 +place/MO-65000,St. Louis,place,state/mo,MO,Missouri,,place/MO-65000 +place/MO-70000,Springfield,place,state/mo,MO,Missouri,,place/MO-70000 +place/MS-36000,Jackson,place,state/ms,MS,Mississippi,,place/MS-36000 +place/MT-06550,Billings,place,state/mt,MT,Montana,,place/MT-06550 +place/NC-10740,Cary,place,state/nc,NC,North Carolina,,place/NC-10740 +place/NC-12000,Charlotte,place,state/nc,NC,North Carolina,,place/NC-12000 +place/NC-14100,Concord,place,state/nc,NC,North Carolina,,place/NC-14100 +place/NC-19000,Durham,place,state/nc,NC,North Carolina,,place/NC-19000 +place/NC-22920,Fayetteville,place,state/nc,NC,North Carolina,,place/NC-22920 +place/NC-28000,Greensboro,place,state/nc,NC,North Carolina,,place/NC-28000 +place/NC-31400,High Point,place,state/nc,NC,North Carolina,,place/NC-31400 +place/NC-55000,Raleigh,place,state/nc,NC,North Carolina,,place/NC-55000 +place/NC-74440,Wilmington,place,state/nc,NC,North Carolina,,place/NC-74440 +place/NC-75000,Winston-Salem,place,state/nc,NC,North Carolina,,place/NC-75000 +place/ND-25700,Fargo,place,state/nd,ND,North Dakota,,place/ND-25700 +place/NE-28000,Lincoln,place,state/ne,NE,Nebraska,,place/NE-28000 +place/NE-37000,Omaha,place,state/ne,NE,Nebraska,,place/NE-37000 +place/NH-45140,Manchester,place,state/nh,NH,New Hampshire,,place/NH-45140 +place/NJ-21000,Elizabeth,place,state/nj,NJ,New Jersey,,place/NJ-21000 +place/NJ-36000,Jersey City,place,state/nj,NJ,New Jersey,,place/NJ-36000 +place/NJ-51000,Newark,place,state/nj,NJ,New Jersey,,place/NJ-51000 +place/NJ-57000,Paterson,place,state/nj,NJ,New Jersey,,place/NJ-57000 +place/NM-02000,Albuquerque,place,state/nm,NM,New Mexico,,place/NM-02000 +place/NM-39380,Las Cruces,place,state/nm,NM,New Mexico,,place/NM-39380 +place/NM-63460,Rio Rancho,place,state/nm,NM,New Mexico,,place/NM-63460 +place/NV-31900,Henderson,place,state/nv,NV,Nevada,,place/NV-31900 +place/NV-40000,Las Vegas,place,state/nv,NV,Nevada,,place/NV-40000 +place/NV-51800,North Las Vegas,place,state/nv,NV,Nevada,,place/NV-51800 +place/NV-60600,Reno,place,state/nv,NV,Nevada,,place/NV-60600 +place/NV-68400,Sparks,place,state/nv,NV,Nevada,,place/NV-68400 +place/NY-01000,Albany,place,state/ny,NY,New York,,place/NY-01000 +place/NY-11000,Buffalo,place,state/ny,NY,New York,,place/NY-11000 +place/NY-51000,New York City,place,state/ny,NY,New York,,place/NY-51000 +place/NY-63000,Rochester,place,state/ny,NY,New York,,place/NY-63000 +place/NY-73000,Syracuse,place,state/ny,NY,New York,,place/NY-73000 +place/NY-84000,Yonkers,place,state/ny,NY,New York,,place/NY-84000 +place/OH-01000,Akron,place,state/oh,OH,Ohio,,place/OH-01000 +place/OH-15000,Cincinnati,place,state/oh,OH,Ohio,,place/OH-15000 +place/OH-16000,Cleveland,place,state/oh,OH,Ohio,,place/OH-16000 +place/OH-18000,Columbus,place,state/oh,OH,Ohio,,place/OH-18000 +place/OH-21000,Dayton,place,state/oh,OH,Ohio,,place/OH-21000 +place/OH-77000,Toledo,place,state/oh,OH,Ohio,,place/OH-77000 +place/OK-09050,Broken Arrow,place,state/ok,OK,Oklahoma,,place/OK-09050 +place/OK-52500,Norman,place,state/ok,OK,Oklahoma,,place/OK-52500 +place/OK-55000,Oklahoma City,place,state/ok,OK,Oklahoma,,place/OK-55000 +place/OK-75000,Tulsa,place,state/ok,OK,Oklahoma,,place/OK-75000 +place/OR-05800,Bend,place,state/or,OR,Oregon,,place/OR-05800 +place/OR-23850,Eugene,place,state/or,OR,Oregon,,place/OR-23850 +place/OR-31250,Gresham,place,state/or,OR,Oregon,,place/OR-31250 +place/OR-34100,Hillsboro,place,state/or,OR,Oregon,,place/OR-34100 +place/OR-59000,Portland,place,state/or,OR,Oregon,,place/OR-59000 +place/OR-64900,Salem,place,state/or,OR,Oregon,,place/OR-64900 +place/PA-02000,Allentown,place,state/pa,PA,Pennsylvania,,place/PA-02000 +place/PA-60000,Philadelphia,place,state/pa,PA,Pennsylvania,,place/PA-60000 +place/PA-61000,Pittsburgh,place,state/pa,PA,Pennsylvania,,place/PA-61000 +place/RI-59000,Providence,place,state/ri,RI,Rhode Island,,place/RI-59000 +place/SC-13330,Charleston,place,state/sc,SC,South Carolina,,place/SC-13330 +place/SC-16000,Columbia,place,state/sc,SC,South Carolina,,place/SC-16000 +place/SC-50875,North Charleston,place,state/sc,SC,South Carolina,,place/SC-50875 +place/SD-59020,Sioux Falls,place,state/sd,SD,South Dakota,,place/SD-59020 +place/TN-14000,Chattanooga,place,state/tn,TN,Tennessee,,place/TN-14000 +place/TN-15160,Clarksville,place,state/tn,TN,Tennessee,,place/TN-15160 +place/TN-40000,Knoxville,place,state/tn,TN,Tennessee,,place/TN-40000 +place/TN-48000,Memphis,place,state/tn,TN,Tennessee,,place/TN-48000 +place/TN-51560,Murfreesboro,place,state/tn,TN,Tennessee,,place/TN-51560 +place/TN-52006,Nashville-Davidson,place,state/tn,TN,Tennessee,,place/TN-52006 +place/TX-01000,Abilene,place,state/tx,TX,Texas,,place/TX-01000 +place/TX-01924,Allen,place,state/tx,TX,Texas,,place/TX-01924 +place/TX-03000,Amarillo,place,state/tx,TX,Texas,,place/TX-03000 +place/TX-04000,Arlington,place,state/tx,TX,Texas,,place/TX-04000 +place/TX-05000,Austin,place,state/tx,TX,Texas,,place/TX-05000 +place/TX-07000,Beaumont,place,state/tx,TX,Texas,,place/TX-07000 +place/TX-10768,Brownsville,place,state/tx,TX,Texas,,place/TX-10768 +place/TX-13024,Carrollton,place,state/tx,TX,Texas,,place/TX-13024 +place/TX-15976,College Station,place,state/tx,TX,Texas,,place/TX-15976 +place/TX-16432,Conroe,place,state/tx,TX,Texas,,place/TX-16432 +place/TX-17000,Corpus Christi,place,state/tx,TX,Texas,,place/TX-17000 +place/TX-19000,Dallas,place,state/tx,TX,Texas,,place/TX-19000 +place/TX-19972,Denton,place,state/tx,TX,Texas,,place/TX-19972 +place/TX-22660,Edinburg,place,state/tx,TX,Texas,,place/TX-22660 +place/TX-24000,El Paso,place,state/tx,TX,Texas,,place/TX-24000 +place/TX-27000,Fort Worth,place,state/tx,TX,Texas,,place/TX-27000 +place/TX-27684,Frisco,place,state/tx,TX,Texas,,place/TX-27684 +place/TX-29000,Garland,place,state/tx,TX,Texas,,place/TX-29000 +place/TX-30464,Grand Prairie,place,state/tx,TX,Texas,,place/TX-30464 +place/TX-35000,Houston,place,state/tx,TX,Texas,,place/TX-35000 +place/TX-37000,Irving,place,state/tx,TX,Texas,,place/TX-37000 +place/TX-39148,Killeen,place,state/tx,TX,Texas,,place/TX-39148 +place/TX-41464,Laredo,place,state/tx,TX,Texas,,place/TX-41464 +place/TX-41980,League City,place,state/tx,TX,Texas,,place/TX-41980 +place/TX-42508,Lewisville,place,state/tx,TX,Texas,,place/TX-42508 +place/TX-45000,Lubbock,place,state/tx,TX,Texas,,place/TX-45000 +place/TX-45384,McAllen,place,state/tx,TX,Texas,,place/TX-45384 +place/TX-45744,McKinney,place,state/tx,TX,Texas,,place/TX-45744 +place/TX-47892,Mesquite,place,state/tx,TX,Texas,,place/TX-47892 +place/TX-48072,Midland,place,state/tx,TX,Texas,,place/TX-48072 +place/TX-50820,New Braunfels,place,state/tx,TX,Texas,,place/TX-50820 +place/TX-53388,Odessa,place,state/tx,TX,Texas,,place/TX-53388 +place/TX-56000,Pasadena,place,state/tx,TX,Texas,,place/TX-56000 +place/TX-56348,Pearland,place,state/tx,TX,Texas,,place/TX-56348 +place/TX-58016,Plano,place,state/tx,TX,Texas,,place/TX-58016 +place/TX-61796,Richardson,place,state/tx,TX,Texas,,place/TX-61796 +place/TX-63500,Round Rock,place,state/tx,TX,Texas,,place/TX-63500 +place/TX-65000,San Antonio,place,state/tx,TX,Texas,,place/TX-65000 +place/TX-70808,Sugar Land,place,state/tx,TX,Texas,,place/TX-70808 +place/TX-74144,Tyler,place,state/tx,TX,Texas,,place/TX-74144 +place/TX-76000,Waco,place,state/tx,TX,Texas,,place/TX-76000 +place/TX-79000,Wichita Falls,place,state/tx,TX,Texas,,place/TX-79000 +place/UT-62470,Provo,place,state/ut,UT,Utah,,place/UT-62470 +place/UT-65330,St. George,place,state/ut,UT,Utah,,place/UT-65330 +place/UT-67000,Salt Lake City,place,state/ut,UT,Utah,,place/UT-67000 +place/UT-82950,West Jordan,place,state/ut,UT,Utah,,place/UT-82950 +place/UT-83470,West Valley City,place,state/ut,UT,Utah,,place/UT-83470 +place/VA-01000,Alexandria,place,state/va,VA,Virginia,,place/VA-01000 +place/VA-16000,Chesapeake,place,state/va,VA,Virginia,,place/VA-16000 +place/VA-35000,Hampton,place,state/va,VA,Virginia,,place/VA-35000 +place/VA-56000,Newport News,place,state/va,VA,Virginia,,place/VA-56000 +place/VA-57000,Norfolk,place,state/va,VA,Virginia,,place/VA-57000 +place/VA-67000,Richmond,place,state/va,VA,Virginia,,place/VA-67000 +place/VA-76432,Suffolk,place,state/va,VA,Virginia,,place/VA-76432 +place/VA-82000,Virginia Beach,place,state/va,VA,Virginia,,place/VA-82000 +place/WA-05210,Bellevue,place,state/wa,WA,Washington,,place/WA-05210 +place/WA-22640,Everett,place,state/wa,WA,Washington,,place/WA-22640 +place/WA-35415,Kent,place,state/wa,WA,Washington,,place/WA-35415 +place/WA-57745,Renton,place,state/wa,WA,Washington,,place/WA-57745 +place/WA-63000,Seattle,place,state/wa,WA,Washington,,place/WA-63000 +place/WA-67000,Spokane,place,state/wa,WA,Washington,,place/WA-67000 +place/WA-67167,Spokane Valley,place,state/wa,WA,Washington,,place/WA-67167 +place/WA-70000,Tacoma,place,state/wa,WA,Washington,,place/WA-70000 +place/WA-74060,Vancouver,place,state/wa,WA,Washington,,place/WA-74060 +place/WI-31000,Green Bay,place,state/wi,WI,Wisconsin,,place/WI-31000 +place/WI-48000,Madison,place,state/wi,WI,Wisconsin,,place/WI-48000 +place/WI-53000,Milwaukee,place,state/wi,WI,Wisconsin,,place/WI-53000 diff --git a/policyengine_api/data/v2/catalog/region_references.py b/policyengine_api/data/v2/catalog/region_references.py new file mode 100644 index 000000000..421e73622 --- /dev/null +++ b/policyengine_api/data/v2/catalog/region_references.py @@ -0,0 +1,259 @@ +"""The Public API's checked-in region reference files, one per country. + +Each file lists every region of one country: its code, label, type, parent, +US state, the value that selects its households in its dataset, and the v1 +identifier it replaces. Catalog publication reads regions from these files, +never from PolicyEngine.py's region registry. +""" + +from __future__ import annotations + +import csv +from collections import Counter +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING + +from policyengine_api.data.v2.catalog.errors import CatalogExtractionError + +if TYPE_CHECKING: + from collections.abc import Iterable, Mapping + +REGION_REFERENCE_DIRECTORY = Path(__file__).parent / "region_reference_files" +REGION_REFERENCE_FIELDS = ( + "code", + "label", + "region_type", + "parent_code", + "state_code", + "state_name", + "filter_value", + "v1_code", +) +SUPPORTED_REGION_TYPES = frozenset( + { + "national", + "country", + "state", + "congressional_district", + "constituency", + "local_authority", + "city", + "place", + } +) +# The catalog's region columns hold at most these many characters. +MAX_REGION_CODE_LENGTH = 255 +MAX_REGION_LABEL_LENGTH = 255 +MAX_REGION_STATE_CODE_LENGTH = 16 +MAX_REGION_STATE_NAME_LENGTH = 128 +MAX_REGION_FILTER_VALUE_LENGTH = 255 + + +@dataclass(frozen=True, slots=True) +class RegionReference: + """One region as its country's reference file lists it. + + ``filter_value`` is the text that selects the region's households: a FIPS + number or district GEOID for US states and districts, the stored region + names of a UK nation, or the ONS code of a UK constituency or local + authority (its area code in the local-area dataset). The national region and regions + that cannot run yet have none. ``v1_code`` is the v1 region identifier this + region replaces, if v1 had one. + """ + + code: str + label: str + region_type: str + parent_code: str | None + state_code: str | None + state_name: str | None + filter_value: str | None + v1_code: str | None + + +def _text( + row: Mapping[str, str | None], + field: str, + *, + code: str, + maximum: int, + required: bool, +) -> str | None: + value = row.get(field) or "" + if value != value.strip(): + msg = f"region {code!r} {field} has surrounding whitespace" + raise CatalogExtractionError(msg) + if not value: + if required: + msg = f"region {code!r} {field} must be a non-empty string" + raise CatalogExtractionError(msg) + return None + if len(value) > maximum: + msg = f"region {code!r} {field} exceeds {maximum} characters" + raise CatalogExtractionError(msg) + return value + + +def _reference(country_id: str, row: Mapping[str, str | None]) -> RegionReference: + code = row.get("code") or "" + if not code or code != code.strip() or len(code) > MAX_REGION_CODE_LENGTH: + msg = f"{country_id} region reference file has an invalid code {code!r}" + raise CatalogExtractionError(msg) + region_type = row.get("region_type") or "" + if region_type not in SUPPORTED_REGION_TYPES: + msg = f"region {code!r} has unsupported type {region_type!r}" + raise CatalogExtractionError(msg) + if region_type != "national" and not code.startswith(f"{region_type}/"): + msg = f"region {code!r} does not start with its type {region_type + '/'!r}" + raise CatalogExtractionError(msg) + return RegionReference( + code=code, + label=_text( + row, "label", code=code, maximum=MAX_REGION_LABEL_LENGTH, required=True + ) + or "", + region_type=region_type, + parent_code=_text( + row, + "parent_code", + code=code, + maximum=MAX_REGION_CODE_LENGTH, + required=False, + ), + state_code=_text( + row, + "state_code", + code=code, + maximum=MAX_REGION_STATE_CODE_LENGTH, + required=False, + ), + state_name=_text( + row, + "state_name", + code=code, + maximum=MAX_REGION_STATE_NAME_LENGTH, + required=False, + ), + filter_value=_text( + row, + "filter_value", + code=code, + maximum=MAX_REGION_FILTER_VALUE_LENGTH, + required=False, + ), + v1_code=_text( + row, "v1_code", code=code, maximum=MAX_REGION_CODE_LENGTH, required=False + ), + ) + + +def _repeated(values: Iterable[str | None]) -> list[str]: + return sorted( + value + for value, count in Counter(value for value in values if value).items() + if count > 1 + ) + + +def _check_parents_present( + country_id: str, + references: tuple[RegionReference, ...], +) -> None: + """Require one parentless national region and a parent for every other region.""" + if not any(reference.code == country_id for reference in references): + msg = ( + f"{country_id} region reference file has no national region {country_id!r}" + ) + raise CatalogExtractionError(msg) + for reference in references: + if reference.region_type == "national": + if reference.code != country_id: + msg = f"national region {reference.code!r} must be named {country_id!r}" + raise CatalogExtractionError(msg) + if reference.parent_code is not None: + msg = f"national region {reference.code!r} must have no parent" + raise CatalogExtractionError(msg) + elif reference.parent_code is None: + msg = f"region {reference.code!r} has no parent" + raise CatalogExtractionError(msg) + + +def _check_ancestry( + country_id: str, + references: tuple[RegionReference, ...], +) -> None: + """Require every region's parents to exist and lead to the national region.""" + parents = {reference.code: reference.parent_code for reference in references} + for reference in references: + visited = {reference.code} + child = reference.code + ancestor = reference.parent_code + while ancestor is not None and ancestor != country_id: + if ancestor not in parents: + raise CatalogExtractionError( + _unknown_ancestor_message(reference.code, child, ancestor) + ) + if ancestor in visited: + msg = f"region {reference.code!r} has a parent cycle" + raise CatalogExtractionError(msg) + visited.add(ancestor) + child, ancestor = ancestor, parents[ancestor] + + +def _unknown_ancestor_message(code: str, child: str, ancestor: str) -> str: + """Name the missing region and, if it is further up, the region it parents.""" + if child == code: + return f"region {code!r} names an unknown parent {ancestor!r}" + return ( + f"region {code!r} descends from {child!r}, whose parent {ancestor!r} is unknown" + ) + + +def parse_region_references( + country_id: str, + rows: Iterable[Mapping[str, str | None]], +) -> tuple[RegionReference, ...]: + """Validate one country's reference rows and return them in file order. + + Codes and v1 identifiers must be unique, every region's code must start + with its type, and every region must descend from the national region. + """ + references = tuple(_reference(country_id, row) for row in rows) + repeated_codes = _repeated(reference.code for reference in references) + if repeated_codes: + msg = ( + f"{country_id} region reference file repeats region codes: {repeated_codes}" + ) + raise CatalogExtractionError(msg) + repeated_v1_codes = _repeated(reference.v1_code for reference in references) + if repeated_v1_codes: + msg = ( + f"{country_id} region reference file repeats v1 identifiers: " + f"{repeated_v1_codes}" + ) + raise CatalogExtractionError(msg) + _check_parents_present(country_id, references) + _check_ancestry(country_id, references) + return references + + +def load_region_references( + country_id: str, + *, + directory: Path = REGION_REFERENCE_DIRECTORY, +) -> tuple[RegionReference, ...]: + """Read and validate the checked-in reference file of one country.""" + path = directory / f"{country_id}.csv" + if not path.is_file(): + msg = f"{country_id} has no region reference file" + raise CatalogExtractionError(msg) + with path.open(encoding="utf-8", newline="") as handle: + reader = csv.DictReader(handle) + if tuple(reader.fieldnames or ()) != REGION_REFERENCE_FIELDS: + msg = ( + f"{country_id} region reference file columns must be " + f"{', '.join(REGION_REFERENCE_FIELDS)}" + ) + raise CatalogExtractionError(msg) + return parse_region_references(country_id, reader) diff --git a/policyengine_api/data/v2/catalog/region_scoping.py b/policyengine_api/data/v2/catalog/region_scoping.py new file mode 100644 index 000000000..3432dcb4c --- /dev/null +++ b/policyengine_api/data/v2/catalog/region_scoping.py @@ -0,0 +1,319 @@ +"""Give each reference region its dataset and filter from the certified data release. + +A scoping rule per country and region type names the release's dataset role +(a key of the release manifest's ``region_datasets``) and the household column +whose value selects the region's households in that dataset. Region types +without a rule cannot run yet, so publication leaves them out and counts them. +""" + +from __future__ import annotations + +import json +from collections import Counter +from collections.abc import Mapping +from dataclasses import dataclass +from typing import TYPE_CHECKING, Protocol, runtime_checkable + +from policyengine_api.data.v2.catalog.errors import CatalogExtractionError +from policyengine_api.data.v2.catalog.records import CertifiedArtifact + +if TYPE_CHECKING: + from policyengine_api.data.v2.catalog.region_references import RegionReference + +ROW_FILTER = "row_filter" +NATIONAL_DATASET_ROLE = "national" + + +@dataclass(frozen=True, slots=True) +class RegionScopingRule: + """How regions of one type find their dataset and select their households. + + ``filter_field`` is the household column a row filter compares with the + region's filter value. When the release names no dataset for the role, + the regions use the default dataset, unless ``requires_release_dataset``: + then only the routed dataset carries the column, and scoping fails. + """ + + dataset_role: str + filter_strategy: str | None = None + filter_field: str | None = None + requires_release_dataset: bool = False + + +REGION_SCOPING_RULES: Mapping[tuple[str, str], RegionScopingRule] = { + ("us", "national"): RegionScopingRule(NATIONAL_DATASET_ROLE), + ("us", "state"): RegionScopingRule("state", ROW_FILTER, "state_fips"), + ("us", "congressional_district"): RegionScopingRule( + "congressional_district", ROW_FILTER, "congressional_district_geoid" + ), + ("uk", "national"): RegionScopingRule(NATIONAL_DATASET_ROLE), + ("uk", "country"): RegionScopingRule(NATIONAL_DATASET_ROLE, ROW_FILTER, "region"), + # The UK national file has no area codes; the release routes constituencies + # and local authorities to a local-area dataset that carries them. + ("uk", "constituency"): RegionScopingRule( + "constituency", + ROW_FILTER, + "constituency_code_oa", + requires_release_dataset=True, + ), + ("uk", "local_authority"): RegionScopingRule( + "local_authority", + ROW_FILTER, + "la_code_oa", + requires_release_dataset=True, + ), +} + + +@dataclass(frozen=True, slots=True) +class ScopedRegion: + """A reference region with its dataset and the filter selecting its households.""" + + reference: RegionReference + dataset_name: str + filter_strategy: str | None + filter_field: str | None + + +@dataclass(frozen=True, slots=True) +class RegionScoping: + """Every runnable region of one country, and what the others fell back to. + + ``fallback_counts`` counts, by region type, regions whose dataset role the + release does not name, so they use the default dataset; ``omitted_counts`` + counts the regions left out because their type cannot run yet. + """ + + regions: tuple[ScopedRegion, ...] + dataset_names: frozenset[str] + certified_artifacts: Mapping[str, CertifiedArtifact] + fallback_counts: Mapping[str, int] + omitted_counts: Mapping[str, int] + + +def _manifest_text(value: object, *, description: str) -> str: + if not isinstance(value, str) or not value: + msg = f"{description} must be a non-empty string" + raise CatalogExtractionError(msg) + return value + + +@runtime_checkable +class ReleaseManifest(Protocol): + """What scoping reads from a PolicyEngine.py country release manifest. + + The manifest is an untyped public object, so each value is checked where + it is read. + """ + + @property + def datasets(self) -> object: + """The certified artifacts by name.""" + ... + + @property + def region_datasets(self) -> object: + """The dataset path of each region type.""" + ... + + @property + def data_package(self) -> object: + """The repository and revision artifacts default to.""" + ... + + +def certified_artifacts( + country_id: str, + release_manifest: ReleaseManifest, +) -> dict[str, CertifiedArtifact]: + """Return the release manifest's artifacts, with the data package's defaults. + + An artifact without its own repository, repository type or revision is in + the data package's repository at the release revision. + """ + data_package = release_manifest.data_package + default_repo_id = getattr(data_package, "repo_id", None) + default_repo_type = getattr(data_package, "repo_type", None) or "model" + default_revision = getattr( + data_package, "release_manifest_revision", None + ) or getattr(data_package, "version", None) + datasets = release_manifest.datasets + if not isinstance(datasets, Mapping): + msg = f"{country_id} release manifest has no dataset map" + raise CatalogExtractionError(msg) + artifacts: dict[str, CertifiedArtifact] = {} + for name, reference in datasets.items(): + description = f"{country_id} release artifact {name}" + artifacts[name] = CertifiedArtifact( + name=_manifest_text(name, description=f"{country_id} release artifact"), + repo_id=_manifest_text( + getattr(reference, "repo_id", None) or default_repo_id, + description=f"{description} repository", + ), + repo_type=_manifest_text( + getattr(reference, "repo_type", None) or default_repo_type, + description=f"{description} repository type", + ), + path=_manifest_text( + getattr(reference, "path", None), description=f"{description} path" + ), + revision=_manifest_text( + getattr(reference, "revision", None) or default_revision, + description=f"{description} revision", + ), + sha256=getattr(reference, "sha256", None), + ) + return artifacts + + +def _role_datasets( + country_id: str, + release_manifest: ReleaseManifest, + artifacts: Mapping[str, CertifiedArtifact], +) -> dict[str, str]: + """Map each dataset role the release names to its certified dataset's name.""" + region_datasets = release_manifest.region_datasets + if not isinstance(region_datasets, Mapping): + msg = f"{country_id} release manifest has no region dataset map" + raise CatalogExtractionError(msg) + names_by_path = {artifact.path: name for name, artifact in artifacts.items()} + role_datasets: dict[str, str] = {} + for role, template in region_datasets.items(): + path = _manifest_text( + getattr(template, "path_template", None), + description=f"{country_id} {role} region dataset", + ) + if "{" in path: + msg = ( + f"{country_id} {role} regions name a per-region dataset template " + f"{path!r}, which the catalog does not support" + ) + raise CatalogExtractionError(msg) + if path not in names_by_path: + msg = ( + f"{country_id} release names no certified dataset at path {path!r} " + f"for {role} regions" + ) + raise CatalogExtractionError(msg) + role_datasets[role] = names_by_path[path] + return role_datasets + + +def _checked_filter_value(reference: RegionReference, rule: RegionScopingRule) -> None: + """Require a filter value exactly when the rule filters, and valid list values.""" + value = reference.filter_value + if rule.filter_strategy is None: + if value is not None: + msg = ( + f"region {reference.code!r} has a filter value, but a national region " + "selects every household" + ) + raise CatalogExtractionError(msg) + return + if value is None: + msg = f"region {reference.code!r} has no filter value" + raise CatalogExtractionError(msg) + if value.startswith(("[", "{")): + try: + members = json.loads(value, parse_constant=_reject_constant) + except ValueError: + members = None + if not ( + isinstance(members, list) + and members + and all(isinstance(member, (str, int, float, bool)) for member in members) + ): + msg = f"region {reference.code!r} has an unsupported filter value" + raise CatalogExtractionError(msg) + + +def _reject_constant(constant: str) -> object: + msg = f"{constant} is not a filter value" + raise ValueError(msg) + + +def scope_regions( + country_id: str, + references: tuple[RegionReference, ...], + release_manifest: object, + *, + reviewed_default: str, +) -> RegionScoping: + """Give every runnable reference region its certified dataset and filter. + + The national region must use the reviewed default dataset. A region whose + type has no scoping rule is left out, and a runnable region may not sit + under a region that is left out. + """ + if not isinstance(release_manifest, ReleaseManifest): + msg = ( + f"{country_id} release manifest lacks datasets, region_datasets or " + "data_package" + ) + raise CatalogExtractionError(msg) + artifacts = certified_artifacts(country_id, release_manifest) + if reviewed_default not in artifacts: + msg = f"{country_id} release does not certify {reviewed_default}" + raise CatalogExtractionError(msg) + role_datasets = _role_datasets(country_id, release_manifest, artifacts) + if role_datasets.get(NATIONAL_DATASET_ROLE, reviewed_default) != reviewed_default: + msg = f"{country_id} national region does not use the reviewed dataset" + raise CatalogExtractionError(msg) + + omitted = { + reference.code: reference.region_type + for reference in references + if (country_id, reference.region_type) not in REGION_SCOPING_RULES + } + fallback_counts: Counter[str] = Counter() + regions: list[ScopedRegion] = [] + for reference in references: + if reference.code in omitted: + continue + if reference.parent_code in omitted: + msg = ( + f"region {reference.code!r} cannot be published because its parent " + f"{reference.parent_code!r} is left out" + ) + raise CatalogExtractionError(msg) + rule = REGION_SCOPING_RULES[country_id, reference.region_type] + _checked_filter_value(reference, rule) + dataset_name = role_datasets.get(rule.dataset_role) + if dataset_name is None: + if rule.requires_release_dataset: + msg = ( + f"the {country_id} release routes no dataset to " + f"{rule.dataset_role} regions, so region {reference.code!r} " + "cannot select its households" + ) + raise CatalogExtractionError(msg) + dataset_name = reviewed_default + if rule.dataset_role != NATIONAL_DATASET_ROLE: + fallback_counts[reference.region_type] += 1 + regions.append( + ScopedRegion( + reference=reference, + dataset_name=dataset_name, + filter_strategy=rule.filter_strategy, + filter_field=rule.filter_field, + ) + ) + return RegionScoping( + regions=tuple(regions), + dataset_names=frozenset( + {reviewed_default, *(region.dataset_name for region in regions)} + ), + certified_artifacts=artifacts, + fallback_counts=dict(sorted(fallback_counts.items())), + omitted_counts=dict(sorted(Counter(omitted.values()).items())), + ) + + +def filtered_artifacts(scoping: RegionScoping) -> tuple[CertifiedArtifact, ...]: + """Return, by name, the datasets the regions' row filters read.""" + names = { + region.dataset_name + for region in scoping.regions + if region.filter_strategy == ROW_FILTER + } + return tuple(scoping.certified_artifacts[name] for name in sorted(names)) diff --git a/policyengine_api/data/v2/models/metadata.py b/policyengine_api/data/v2/models/metadata.py index 8d91864df..37f04f7be 100644 --- a/policyengine_api/data/v2/models/metadata.py +++ b/policyengine_api/data/v2/models/metadata.py @@ -111,8 +111,9 @@ class Region(TimestampedModel, table=True): """A region in one model version, unique by code within that version. Its default dataset must belong to the same version. Filtered regions - (``requires_filter``) also name the variable and value that select their - households. + (``requires_filter``) also name how they select their households: a + ``row_filter`` keeps the households whose ``filter_field`` column matches + ``filter_value``. """ __tablename__ = "regions" diff --git a/policyengine_api/services/v2/metadata/types.py b/policyengine_api/services/v2/metadata/types.py index 7f41e1575..a7336d7e9 100644 --- a/policyengine_api/services/v2/metadata/types.py +++ b/policyengine_api/services/v2/metadata/types.py @@ -136,8 +136,10 @@ class MetadataDataset(StrictResponseModel): class MetadataRegion(StrictResponseModel): """A region the model can simulate, such as a country, state or constituency. - When requires_filter is true, the region is simulated by filtering its - default dataset on filter_field and filter_value. + When requires_filter is true, filter_strategy says how the region selects + households of its default dataset. With "row_filter", it keeps the + households whose filter_field column equals filter_value (or is in it, + when filter_value is a JSON list). """ id: UUID diff --git a/pyproject.toml b/pyproject.toml index ef1bee9d6..9285d2383 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,10 @@ dependencies = [ "google-cloud-logging>=3,<4", "google-cloud-secret-manager>=2,<3", "gunicorn", + # Catalog publication reads certified HDF5 artifacts from Hugging Face. + "h5py>=3.16,<4", "httpx>=0.27.0", + "huggingface-hub>=1.12,<2", "markupsafe>=3,<4", "microdf_python>=1.0.0", "openai", diff --git a/tests/fixtures/v2_catalog.py b/tests/fixtures/v2_catalog.py index 77f5cbd20..61e569f95 100644 --- a/tests/fixtures/v2_catalog.py +++ b/tests/fixtures/v2_catalog.py @@ -2,12 +2,12 @@ from __future__ import annotations -from dataclasses import dataclass from datetime import UTC, datetime from types import SimpleNamespace from typing import TYPE_CHECKING from policyengine_api.data.v2.catalog.extraction import extract_catalog +from policyengine_api.data.v2.catalog.region_references import RegionReference if TYPE_CHECKING: from policyengine_api.data.v2.catalog.records import NormalizedCatalog @@ -20,49 +20,164 @@ } -class RegionRegistry(list): - """Small iterable with the public registry's country identifier.""" +def region_references() -> dict[str, tuple[RegionReference, ...]]: + """Return small US and UK reference files. - def __init__(self, country_id: str, regions: list[SimpleNamespace]): - super().__init__(regions) - self.country_id = country_id + The US place has no filter rule, so publication leaves it out. + """ + return { + "us": ( + RegionReference( + code="us", + label="United States", + region_type="national", + parent_code=None, + state_code=None, + state_name=None, + filter_value=None, + v1_code="us", + ), + RegionReference( + code="state/ca", + label="California", + region_type="state", + parent_code="us", + state_code="CA", + state_name="California", + filter_value="6", + v1_code="state/ca", + ), + RegionReference( + code="congressional_district/CA-01", + label="California's 1st congressional district", + region_type="congressional_district", + parent_code="state/ca", + state_code="CA", + state_name="California", + filter_value="601", + v1_code="congressional_district/CA-01", + ), + RegionReference( + code="place/CA-44000", + label="Los Angeles", + region_type="place", + parent_code="state/ca", + state_code="CA", + state_name="California", + filter_value=None, + v1_code="place/CA-44000", + ), + ), + "uk": ( + RegionReference( + code="uk", + label="United Kingdom", + region_type="national", + parent_code=None, + state_code=None, + state_name=None, + filter_value=None, + v1_code="uk", + ), + RegionReference( + code="country/england", + label="England", + region_type="country", + parent_code="uk", + state_code=None, + state_name=None, + filter_value='["NORTH_EAST","LONDON"]', + v1_code="country/england", + ), + RegionReference( + code="constituency/E14001063", + label="Aldershot", + region_type="constituency", + parent_code="country/england", + state_code=None, + state_name=None, + filter_value="E14001063", + v1_code="constituency/Aldershot", + ), + RegionReference( + code="country/northern_ireland", + label="Northern Ireland", + region_type="country", + parent_code="uk", + state_code=None, + state_name=None, + filter_value="NORTHERN_IRELAND", + v1_code="country/ni", + ), + RegionReference( + code="local_authority/N09000011", + label="Ards and North Down", + region_type="local_authority", + parent_code="country/northern_ireland", + state_code=None, + state_name=None, + filter_value="N09000011", + v1_code="local_authority/Ards and North Down", + ), + ), + } -def _strategy(field: str, value: str | int) -> SimpleNamespace: +def _certified_artifact(path: str, *, revision: str = "fixture") -> SimpleNamespace: return SimpleNamespace( - strategy_type="row_filter", - variable_name=field, - variable_value=value, - additional_filters={}, + path=path, + revision=revision, + sha256="0" * 64, + repo_id=None, + repo_type=None, ) -@dataclass(frozen=True, kw_only=True) -class _Region: - """One public region; ``source`` gives the mutable object the registry holds.""" - - code: str - label: str - region_type: str - parent_code: str | None = None - dataset_path: str | None = None - strategy: SimpleNamespace | None = None - state_code: str | None = None - state_name: str | None = None - - def source(self) -> SimpleNamespace: - """Return the region as a registry entry; it needs a filter if scoped.""" - return SimpleNamespace( - code=self.code, - label=self.label, - region_type=self.region_type, - parent_code=self.parent_code, - dataset_path=self.dataset_path, - scoping_strategy=self.strategy, - requires_filter=self.strategy is not None, - state_code=self.state_code, - state_name=self.state_name, +def _release_contents(country_id: str) -> tuple[dict, dict, SimpleNamespace]: + """Return a country's certified datasets, region datasets and data package.""" + if country_id == "us": + datasets = { + "populace_us_2024": _certified_artifact("populace_us_2024.h5"), + "populace_us_2024_acs_local": _certified_artifact( + "populace_us_2024_acs_local.h5" + ), + } + region_datasets = { + "national": SimpleNamespace(path_template="populace_us_2024.h5"), + "state": SimpleNamespace(path_template="populace_us_2024_acs_local.h5"), + "congressional_district": SimpleNamespace( + path_template="populace_us_2024_acs_local.h5" + ), + } + data_package = SimpleNamespace( + repo_id="policyengine/populace-us", + repo_type="dataset", + version="0.1.0", + release_manifest_revision="fixture", ) + return datasets, region_datasets, data_package + datasets = { + "microcosm_uk_2024_25": _certified_artifact("microcosm_uk_2024_25.h5"), + "microcosm_uk_2024_25_dense": _certified_artifact( + "microcosm_uk_2024_25_dense.h5" + ), + } + # As in PolicyEngine.py 6.10.0, constituencies and local authorities are + # routed to the local-area dataset, which carries their codes. + region_datasets = { + "national": SimpleNamespace(path_template="microcosm_uk_2024_25.h5"), + "constituency": SimpleNamespace(path_template="microcosm_uk_2024_25_dense.h5"), + "local_authority": SimpleNamespace( + path_template="microcosm_uk_2024_25_dense.h5" + ), + } + data_package = SimpleNamespace( + repo_id="example-org/example-uk-data", + repo_type="model", + version="1.0.0", + release_manifest_revision="fixture", + ) + return datasets, region_datasets, data_package def bundle(*, policyengine_version: str = POLICYENGINE_VERSION) -> dict: @@ -88,7 +203,6 @@ def _model_source( country_id: str, country_package_version: str, policyengine_version: str, - regions: list[SimpleNamespace], ) -> SimpleNamespace: model_name = f"policyengine-{country_id}" default_dataset = { @@ -141,13 +255,16 @@ def _model_source( name=model_name, version=country_package_version, ) + datasets, region_datasets, data_package = _release_contents(country_id) version_holder.release_manifest = SimpleNamespace( country_id=country_id, policyengine_version=policyengine_version, default_dataset=default_dataset, default_dataset_uri=default_uri, + datasets=datasets, + region_datasets=region_datasets, + data_package=data_package, ) - version_holder.region_registry = RegionRegistry(country_id, regions) version_holder.variables = [variable] version_holder.parameter_nodes = [parameter_node] version_holder.parameters = [parameter] @@ -162,60 +279,13 @@ def source_models( policyengine_version: str = POLICYENGINE_VERSION, ) -> dict[str, SimpleNamespace]: """Return US and UK public-model fixtures.""" - us_default_uri = "hf://policyengine/us/populace_us_2024.h5@fixture" - uk_default_uri = "hf://policyengine/uk/microcosm_uk_2024_25.h5@fixture" return { - "us": _model_source( - country_id="us", - country_package_version=DEPENDENCY_VERSIONS["policyengine-us"], - policyengine_version=policyengine_version, - regions=[ - _Region( - code="us", - label="United States", - region_type="national", - dataset_path=us_default_uri, - ).source(), - _Region( - code="state/ca", - label="California", - region_type="state", - parent_code="us", - dataset_path="hf://policyengine/us/populace_us_ca_2024.h5@fixture", - strategy=_strategy("state_fips", 6), - state_code="CA", - state_name="California", - ).source(), - _Region( - code="place/CA-44000", - label="Los Angeles", - region_type="place", - parent_code="state/ca", - state_code="CA", - state_name="California", - ).source(), - ], - ), - "uk": _model_source( - country_id="uk", - country_package_version=DEPENDENCY_VERSIONS["policyengine-uk"], + country_id: _model_source( + country_id=country_id, + country_package_version=DEPENDENCY_VERSIONS[f"policyengine-{country_id}"], policyengine_version=policyengine_version, - regions=[ - _Region( - code="uk", - label="United Kingdom", - region_type="national", - dataset_path=uk_default_uri, - ).source(), - _Region( - code="country/england", - label="England", - region_type="country", - parent_code="uk", - strategy=_strategy("country", "ENGLAND"), - ).source(), - ], - ), + ) + for country_id in ("us", "uk") } @@ -234,4 +304,5 @@ def normalized_catalog( policyengine_version=policyengine_version, models=source_models(policyengine_version=policyengine_version), installed_version=installed_version, + region_references=region_references(), ) diff --git a/tests/integration/test_v2_catalog_installed.py b/tests/integration/test_v2_catalog_installed.py index 0f9106e06..b705d68d0 100644 --- a/tests/integration/test_v2_catalog_installed.py +++ b/tests/integration/test_v2_catalog_installed.py @@ -4,13 +4,18 @@ import json import os +from collections import Counter from importlib import metadata as importlib_metadata +from typing import TYPE_CHECKING import pytest from policyengine.bundle import get_current_bundle from policyengine_api.data.v2.catalog.extraction import extract_installed_catalog +if TYPE_CHECKING: + from policyengine_api.data.v2.catalog.records import CountryCatalog + pytestmark = pytest.mark.skipif( os.environ.get("RUN_V2_CATALOG_COMPATIBILITY") != "1", reason=("loads the complete public catalog; set RUN_V2_CATALOG_COMPATIBILITY=1"), @@ -38,10 +43,9 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: ) # Counts for the 6.10.0 bundle's models: US 2.2.1 and UK 2.132.6. Keep # this exact assertion so later package changes require explicit review. - # Since 6.10.0 the UK registry also lists its 650 constituencies and 361 - # local authorities, because the bundle routes them to a local-area - # release, so regions grow by 1,011 from 6.7.5's 826 and the catalog - # adds that release's dataset. + # Regions come from the checked-in reference files: 488 US (places are + # left out) and 1,016 UK, whose constituencies and local authorities use + # the local-area dataset the release routes them to. assert catalog.entity_counts() == { "models": 2, "model_versions": 2, @@ -50,7 +54,7 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: "parameters": 104_172, "parameter_values": 1_194_360, "datasets": 4, - "regions": 1_837, + "regions": 1_504, } for country_id in ("us", "uk"): @@ -73,18 +77,18 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: total_values += len(batch) assert total_values == country.entity_counts()["parameter_values"] - assert {dataset.name for dataset in catalog.country("us").datasets} == { - "populace_us_2024", - "populace_us_2024_acs_local", - } - assert {dataset.name for dataset in catalog.country("uk").datasets} == { + _check_uk_regions(catalog.country("uk")) + _check_us_regions(catalog.country("us")) + + +def _check_uk_regions(uk_catalog: CountryCatalog) -> None: + assert {dataset.name for dataset in uk_catalog.datasets} == { "microcosm_uk_2024_25", "microcosm_uk_2024_25_dense", } # The national file has no area codes, so UK constituencies and local # authorities use the local-area dataset .py routes them to, while the # UK countries filter the national file. - uk_catalog = catalog.country("uk") uk_dataset_names = {dataset.id: dataset.name for dataset in uk_catalog.datasets} uk_region_type_datasets: dict[str, set[str]] = {} for region in uk_catalog.regions: @@ -99,9 +103,7 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: } assert uk_catalog.fallback_summaries == () england = next( - region - for region in catalog.country("uk").regions - if region.code == "country/england" + region for region in uk_catalog.regions if region.code == "country/england" ) assert england.filter_field == "region" assert england.filter_strategy == "row_filter" @@ -118,11 +120,39 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: "SOUTH_WEST", ] + uk_types = Counter(region.region_type for region in uk_catalog.regions) + assert uk_types == { + "national": 1, + "country": 4, + "constituency": 650, + "local_authority": 361, + } + uk_regions = {region.code: region for region in uk_catalog.regions} + aldershot = uk_regions["constituency/E14001063"] + assert ( + aldershot.filter_strategy, + aldershot.filter_field, + aldershot.filter_value, + ) == ("row_filter", "constituency_code_oa", "E14001063") + ards = uk_regions["local_authority/N09000011"] + assert (ards.label, ards.filter_field, ards.filter_value) == ( + "Ards and North Down", + "la_code_oa", + "N09000011", + ) + assert uk_regions["uk"].label == "United Kingdom" + + +def _check_us_regions(us_catalog: CountryCatalog) -> None: + assert {dataset.name for dataset in us_catalog.datasets} == { + "populace_us_2024", + "populace_us_2024_acs_local", + } # Since 6.7.5, US state and congressional-district regions use the # ACS-local dataset again, on the PolicyEngine/microcosm#1263 rebuild # (PolicyEngine/policyengine.py#649, ending #604's national-file filter). - # The national region uses the national dataset, and places fall back to it. - us_catalog = catalog.country("us") + # The national region uses the national dataset. Regions come from the + # checked-in reference files; US places cannot run yet and are left out. us_dataset_names = {dataset.id: dataset.name for dataset in us_catalog.datasets} region_type_datasets: dict[str, set[str]] = {} for region in us_catalog.regions: @@ -133,11 +163,13 @@ def test_installed_policyengine_catalog_is_complete_and_bounded() -> None: "national": {"populace_us_2024"}, "state": {"populace_us_2024_acs_local"}, "congressional_district": {"populace_us_2024_acs_local"}, - "place": {"populace_us_2024"}, } + assert us_catalog.fallback_summaries == () assert [ (summary.region_type, summary.count) - for summary in us_catalog.fallback_summaries - ] == [ - ("place", 333), - ] + for summary in us_catalog.omitted_region_summaries + ] == [("place", 333)] + + assert {region.code: region.label for region in us_catalog.regions}["us"] == ( + "United States" + ) diff --git a/tests/integration/test_v2_catalog_publication.py b/tests/integration/test_v2_catalog_publication.py index 25bc9178f..edef7b93e 100644 --- a/tests/integration/test_v2_catalog_publication.py +++ b/tests/integration/test_v2_catalog_publication.py @@ -15,9 +15,11 @@ from sqlalchemy.pool import NullPool from sqlmodel import Session, select +from policyengine_api.data.v2.catalog import publication as publication_module from policyengine_api.data.v2.catalog.publication import ( CatalogPublicationError, publish_catalog, + unpublished_countries, ) from policyengine_api.data.v2.models import ( Dataset, @@ -121,6 +123,7 @@ def test_publish_and_republish_preserve_complete_catalog( second = publish_catalog(publication_engine, catalog) assert first.as_dict()["outcome"] == "ok" + assert first.omitted_region_summaries == (("us", "place", 1),) assert second.policyengine_version == first.policyengine_version assert ( _counts(publication_engine) @@ -132,9 +135,9 @@ def test_publish_and_republish_preserve_complete_catalog( "parameter_nodes": 2, "parameters": 2, "parameter_values": 4, - "datasets": 3, + "datasets": 4, "dataset_versions": 0, - "regions": 5, + "regions": 8, "simulations": 0, "reports": 0, "report_runs": 0, @@ -163,14 +166,44 @@ def test_publish_and_republish_preserve_complete_catalog( storage_path is None and not output for _, storage_path, output in datasets ) assert dict(region_defaults) == { + "congressional_district/CA-01": "populace_us_2024_acs_local", + "constituency/E14001063": "microcosm_uk_2024_25_dense", "country/england": "microcosm_uk_2024_25", - "place/CA-44000": "populace_us_2024", - "state/ca": "populace_us_ca_2024", + "country/northern_ireland": "microcosm_uk_2024_25", + "local_authority/N09000011": "microcosm_uk_2024_25_dense", + "state/ca": "populace_us_2024_acs_local", "uk": "microcosm_uk_2024_25", "us": "populace_us_2024", } +def test_unpublished_country_check_reports_a_missing_schema_specifically() -> None: + # The same server's maintenance database has no v2 schema at all. + schema_less = make_url(_disposable_url()).set(database="postgres") + engine = create_engine(schema_less, poolclass=NullPool) + try: + with pytest.raises( + CatalogPublicationError, + match=r"^the v2 Alembic revision table is absent$", + ): + unpublished_countries(engine, normalized_catalog()) + finally: + engine.dispose() + + +def test_lists_only_countries_whose_catalog_version_is_not_stored( + publication_engine: Engine, +) -> None: + catalog = normalized_catalog() + newer = normalized_catalog(policyengine_version="4.21.0") + + assert unpublished_countries(publication_engine, catalog) == ("us", "uk") + publish_catalog(publication_engine, catalog) + + assert unpublished_countries(publication_engine, catalog) == () + assert unpublished_countries(publication_engine, newer) == ("us", "uk") + + @pytest.mark.parametrize( "failure_point", ["during_copy", "after_reconciliation", "after_validation"], @@ -209,13 +242,190 @@ def test_same_version_with_changed_content_fails_without_mutation( countries=(changed_us, catalog.country("uk")), ) - with pytest.raises(CatalogPublicationError, match="differs"): + with pytest.raises( + CatalogPublicationError, + match=r"^the stored us catalog version 5\.0\.4 differs from the staged " + r"catalog in its variables$", + ): publish_catalog(publication_engine, changed) assert _identifiers(publication_engine) == before_ids assert _counts(publication_engine) == before_counts +def test_stored_version_with_a_different_dataset_names_the_datasets( + publication_engine: Engine, +) -> None: + catalog = normalized_catalog() + publish_catalog(publication_engine, catalog) + uk = catalog.country("uk") + changed = replace( + catalog, + countries=( + catalog.country("us"), + replace( + uk, + datasets=tuple( + replace(dataset, description="Another dataset selection") + for dataset in uk.datasets + ), + ), + ), + ) + + with pytest.raises( + CatalogPublicationError, + match=r"^the stored uk catalog version 5\.0\.4 differs from the staged " + r"catalog in its datasets$", + ): + publish_catalog(publication_engine, changed) + + +def _region_rows(engine: Engine) -> list[tuple[object, ...]]: + with engine.connect() as connection: + return [ + tuple(row) + for row in connection.execute( + text( + """ + SELECT id, code, label, region_type, requires_filter, + filter_field, filter_value, filter_strategy, + parent_code, state_code, state_name, default_dataset_id + FROM regions + ORDER BY id + """ + ) + ) + ] + + +def _with_changed_us_regions(catalog): + """Relabel state/ca and drop the district, as a reference-file change would.""" + us = catalog.country("us") + regions = tuple( + replace(region, label="Changed California") + if region.code == "state/ca" + else region + for region in us.regions + if region.code != "congressional_district/CA-01" + ) + return replace( + catalog, + countries=(replace(us, regions=regions), catalog.country("uk")), + ) + + +def test_stored_version_keeps_its_published_regions_and_logs_the_difference( + publication_engine: Engine, + monkeypatch: pytest.MonkeyPatch, +) -> None: + catalog = normalized_catalog() + publish_catalog(publication_engine, catalog) + before_regions = _region_rows(publication_engine) + before_ids = _identifiers(publication_engine) + before_counts = _counts(publication_engine) + # Alembic's logging configuration disables existing loggers, so the + # warnings are recorded where they are emitted. + warnings: list[tuple[str, dict]] = [] + monkeypatch.setattr( + publication_module.LOGGER, + "warning", + lambda message, *args, **kwargs: warnings.append((message % args, kwargs)), + ) + + evidence = publish_catalog(publication_engine, _with_changed_us_regions(catalog)) + + assert evidence.as_dict()["outcome"] == "ok" + assert _region_rows(publication_engine) == before_regions + assert _identifiers(publication_engine) == before_ids + assert _counts(publication_engine) == before_counts + # The evidence describes the stored rows, not the staged regions. + assert evidence.entity_counts == { + "models": before_counts["tax_benefit_models"], + "model_versions": before_counts["tax_benefit_model_versions"], + "variables": before_counts["variables"], + "parameter_nodes": before_counts["parameter_nodes"], + "parameters": before_counts["parameters"], + "parameter_values": before_counts["parameter_values"], + "datasets": before_counts["datasets"], + "regions": before_counts["regions"], + } + assert evidence.kept_region_summaries == ( + ("us", catalog.policyengine_version, 3, 2), + ) + assert evidence.omitted_region_summaries == () + assert evidence.fallback_summaries == () + notices = [ + (message, keywords["extra"]) + for message, keywords in warnings + if message.startswith("kept the published regions") + ] + assert [message for message, _extra in notices] == [ + "kept the published regions of the stored us catalog version " + f"{catalog.policyengine_version}: 1 staged region rows are not stored and " + "2 stored region rows are not staged" + ] + assert [extra["policyengine_attributes"] for _message, extra in notices] == [ + { + "country_id": "us", + "policyengine_version": catalog.policyengine_version, + "staged_region_rows_not_stored": 1, + "stored_region_rows_not_staged": 2, + } + ] + + +def test_stored_version_with_changed_regions_still_fails_on_other_tables( + publication_engine: Engine, +) -> None: + catalog = normalized_catalog() + publish_catalog(publication_engine, catalog) + before_regions = _region_rows(publication_engine) + changed = _with_changed_us_regions(catalog) + us = changed.country("us") + changed = replace( + changed, + countries=( + replace(us, variables=(replace(us.variables[0], label="Changed"),)), + changed.country("uk"), + ), + ) + + with pytest.raises(CatalogPublicationError, match="differs"): + publish_catalog(publication_engine, changed) + + assert _region_rows(publication_engine) == before_regions + + +def test_new_version_publishes_the_reference_file_regions( + publication_engine: Engine, +) -> None: + publish_catalog(publication_engine, normalized_catalog()) + newer = _with_changed_us_regions(normalized_catalog(policyengine_version="4.21.0")) + + publish_catalog(publication_engine, newer) + + with publication_engine.connect() as connection: + newer_us_regions = dict( + connection.execute( + text( + """ + SELECT region.code, region.label + FROM regions AS region + JOIN tax_benefit_model_versions AS version + ON version.id = region.tax_benefit_model_version_id + JOIN tax_benefit_models AS model ON model.id = version.model_id + WHERE version.version = '4.21.0' AND model.name = 'policyengine-us' + """ + ) + ).all() + ) + assert newer_us_regions == { + "state/ca": "Changed California", + "us": "United States", + } + + def test_republish_preserves_existing_run_and_dataset_version_records( publication_engine: Engine, ) -> None: @@ -333,9 +543,9 @@ def test_new_version_is_additive_and_concurrent_retries_serialize( "parameter_nodes": 4, "parameters": 4, "parameter_values": 8, - "datasets": 6, + "datasets": 8, "dataset_versions": 0, - "regions": 10, + "regions": 16, "simulations": 0, "reports": 0, "report_runs": 0, diff --git a/tests/unit/v2/test_catalog_extraction.py b/tests/unit/v2/test_catalog_extraction.py index f88319cd8..bc6d45eca 100644 --- a/tests/unit/v2/test_catalog_extraction.py +++ b/tests/unit/v2/test_catalog_extraction.py @@ -3,7 +3,7 @@ from __future__ import annotations import ast -import json +from dataclasses import replace from datetime import UTC, datetime from importlib.metadata import PackageNotFoundError from pathlib import Path @@ -22,6 +22,7 @@ bundle, installed_version, normalized_catalog, + region_references, source_models, ) @@ -39,9 +40,12 @@ def test_extracts_canonical_policyengine_catalog_and_dataset_defaults() -> None: assert {dataset.name for dataset in us.datasets} == { "populace_us_2024", - "populace_us_ca_2024", + "populace_us_2024_acs_local", + } + assert {dataset.name for dataset in uk.datasets} == { + "microcosm_uk_2024_25", + "microcosm_uk_2024_25_dense", } - assert {dataset.name for dataset in uk.datasets} == {"microcosm_uk_2024_25"} assert all( not dataset.is_output_dataset for dataset in (*us.datasets, *uk.datasets) ) @@ -52,74 +56,138 @@ def test_extracts_canonical_policyengine_catalog_and_dataset_defaults() -> None: region.code: us_datasets[region.default_dataset_id] for region in us.regions } assert us_defaults == { - "place/CA-44000": "populace_us_2024", - "state/ca": "populace_us_ca_2024", + "congressional_district/CA-01": "populace_us_2024_acs_local", + "state/ca": "populace_us_2024_acs_local", "us": "populace_us_2024", } + assert us.fallback_summaries == () assert [ - (summary.region_type, summary.count) for summary in us.fallback_summaries + (summary.region_type, summary.count) for summary in us.omitted_region_summaries ] == [("place", 1)] - assert all(region.default_dataset_id == uk.datasets[0].id for region in uk.regions) - + assert uk.omitted_region_summaries == () + uk_datasets = {dataset.id: dataset.name for dataset in uk.datasets} + assert { + region.code: uk_datasets[region.default_dataset_id] for region in uk.regions + } == { + "uk": "microcosm_uk_2024_25", + "country/england": "microcosm_uk_2024_25", + "country/northern_ireland": "microcosm_uk_2024_25", + "constituency/E14001063": "microcosm_uk_2024_25_dense", + "local_authority/N09000011": "microcosm_uk_2024_25_dense", + } + # UK nations filter the national dataset by design, and constituencies and + # local authorities use the dataset the release routes them to. + assert uk.fallback_summaries == () -UK_DENSE_URI = "hf://policyengine/uk/microcosm_uk_2024_25_dense.h5@fixture" +def test_regions_take_labels_from_references_and_filters_from_scoping_rules() -> None: + catalog = normalized_catalog() + regions = { + region.code: ( + region.label, + region.region_type, + region.parent_code, + region.requires_filter, + region.filter_strategy, + region.filter_field, + region.filter_value, + region.state_code, + region.state_name, + ) + for country_id in ("us", "uk") + for region in catalog.country(country_id).regions + } -def _uk_area_region(code: str, region_type: str, filter_field: str) -> SimpleNamespace: - """Return a UK area region routed to the local-area dataset, as .py 6.10 does.""" - return SimpleNamespace( - code=code, - label=code.split("/", maxsplit=1)[1], - region_type=region_type, - parent_code="uk", - dataset_path=UK_DENSE_URI, - scoping_strategy=SimpleNamespace( - strategy_type="row_filter", - variable_name=filter_field, - variable_value=code.split("/", maxsplit=1)[1], - additional_filters={}, + assert regions == { + "us": ("United States", "national", None, False, None, None, None, None, None), + "state/ca": ( + "California", + "state", + "us", + True, + "row_filter", + "state_fips", + "6", + "CA", + "California", ), - requires_filter=True, - state_code=None, - state_name=None, - ) + "congressional_district/CA-01": ( + "California's 1st congressional district", + "congressional_district", + "state/ca", + True, + "row_filter", + "congressional_district_geoid", + "601", + "CA", + "California", + ), + "uk": ("United Kingdom", "national", None, False, None, None, None, None, None), + "country/england": ( + "England", + "country", + "uk", + True, + "row_filter", + "region", + '["NORTH_EAST","LONDON"]', + None, + None, + ), + "constituency/E14001063": ( + "Aldershot", + "constituency", + "country/england", + True, + "row_filter", + "constituency_code_oa", + "E14001063", + None, + None, + ), + "country/northern_ireland": ( + "Northern Ireland", + "country", + "uk", + True, + "row_filter", + "region", + "NORTHERN_IRELAND", + None, + None, + ), + "local_authority/N09000011": ( + "Ards and North Down", + "local_authority", + "country/northern_ireland", + True, + "row_filter", + "la_code_oa", + "N09000011", + None, + None, + ), + } -def test_uk_area_regions_use_the_dataset_their_path_names() -> None: +def test_sub_national_regions_without_a_release_dataset_use_the_default() -> None: models = source_models() - models["uk"].region_registry.extend( - [ - _uk_area_region( - "constituency/E14001063", "constituency", "constituency_code_oa" - ), - _uk_area_region( - "local_authority/E07000092", "local_authority", "la_code_oa" - ), - ] - ) + del models["us"].release_manifest.region_datasets["state"] + del models["us"].release_manifest.region_datasets["congressional_district"] - uk = extract_catalog( + us = extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, installed_version=installed_version, - ).country("uk") + ).country("us") - assert {dataset.name for dataset in uk.datasets} == { - "microcosm_uk_2024_25", - "microcosm_uk_2024_25_dense", - } - uk_datasets = {dataset.id: dataset.name for dataset in uk.datasets} - assert { - region.code: uk_datasets[region.default_dataset_id] for region in uk.regions - } == { - "uk": "microcosm_uk_2024_25", - "country/england": "microcosm_uk_2024_25", - "constituency/E14001063": "microcosm_uk_2024_25_dense", - "local_authority/E07000092": "microcosm_uk_2024_25_dense", - } - # A UK country filters the national dataset by design; it is no fallback. - assert uk.fallback_summaries == () + assert {dataset.name for dataset in us.datasets} == {"populace_us_2024"} + assert all(region.default_dataset_id == us.datasets[0].id for region in us.regions) + assert [ + (summary.region_type, summary.count) for summary in us.fallback_summaries + ] == [("congressional_district", 1), ("state", 1)] def test_durable_ids_are_deterministic_and_stable_at_the_intended_scope() -> None: @@ -179,62 +247,56 @@ def item(self): assert normalize_json_value(float("-inf")) == "-Infinity" -@pytest.mark.parametrize("value", [("NORTH_EAST", "LONDON"), ["NORTH_EAST", "LONDON"]]) -def test_serializes_region_membership_filters_in_the_existing_text_field( - value: object, -) -> None: - models = source_models() - models["uk"].region_registry[1].scoping_strategy.variable_value = value - - catalog = extract_catalog( - bundle=bundle(), - policyengine_version=POLICYENGINE_VERSION, - models=models, - installed_version=installed_version, +@pytest.mark.parametrize("value", ['["NORTH_EAST","LONDON"]', "ENGLAND", "6"]) +def test_keeps_reference_filter_values_as_text(value: str) -> None: + references = region_references() + england = references["uk"][1] + references["uk"] = ( + references["uk"][0], + replace(england, filter_value=value), + references["uk"][2], ) - england = next( - region - for region in catalog.country("uk").regions - if region.code == "country/england" - ) - - assert isinstance(england.filter_value, str) - assert json.loads(england.filter_value) == ["NORTH_EAST", "LONDON"] - - -@pytest.mark.parametrize("value", ["ENGLAND", 6, True, 1.5]) -def test_preserves_existing_scalar_region_filter_serialization(value: object) -> None: - models = source_models() - models["uk"].region_registry[1].scoping_strategy.variable_value = value catalog = extract_catalog( + region_references=references, bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, - models=models, + models=source_models(), installed_version=installed_version, ) - england = next( + england_record = next( region for region in catalog.country("uk").regions if region.code == "country/england" ) - assert england.filter_value == str(value) + assert england_record.filter_value == value @pytest.mark.parametrize( "value", - [[], [["LONDON"]], [None], [object()], [float("nan")], ["x" * 256], "x" * 256], + ["[]", '[["LONDON"]]', "[null]", '[{"a": 1}]', "[NaN]", "[", '{"a": 1}'], ) -def test_rejects_invalid_or_oversized_region_membership_values(value: object) -> None: - models = source_models() - models["uk"].region_registry[1].scoping_strategy.variable_value = value +def test_rejects_filter_value_lists_that_are_not_flat_json_scalars( + value: str, +) -> None: + references = region_references() + england = references["uk"][1] + references["uk"] = ( + references["uk"][0], + replace(england, filter_value=value), + references["uk"][2], + ) - with pytest.raises(CatalogExtractionError, match="filter value"): + with pytest.raises( + CatalogExtractionError, + match=r"^region 'country/england' has an unsupported filter value$", + ): extract_catalog( + region_references=references, bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, - models=models, + models=source_models(), installed_version=installed_version, ) @@ -250,6 +312,7 @@ def test_rejects_missing_or_mismatched_manifest_dependencies() -> None: del missing["packages"]["policyengine-core"] with pytest.raises(CatalogExtractionError, match="omits policyengine-core"): extract_catalog( + region_references=region_references(), bundle=missing, policyengine_version=POLICYENGINE_VERSION, models=source_models(), @@ -258,6 +321,7 @@ def test_rejects_missing_or_mismatched_manifest_dependencies() -> None: with pytest.raises(CatalogExtractionError, match="does not match"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=source_models(), @@ -271,6 +335,7 @@ def absent(_name: str) -> str: with pytest.raises(CatalogExtractionError, match="is absent"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=source_models(), @@ -282,6 +347,7 @@ def absent(_name: str) -> str: def test_rejects_placeholder_policyengine_versions(version: str) -> None: with pytest.raises(CatalogExtractionError, match="placeholder"): extract_catalog( + region_references=region_references(), bundle=bundle(policyengine_version=version), policyengine_version=version, models=source_models(policyengine_version=version), @@ -294,6 +360,7 @@ def test_rejects_incomplete_public_model_catalogs() -> None: del models["uk"] with pytest.raises(CatalogExtractionError, match="absent for: uk"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -304,6 +371,7 @@ def test_rejects_incomplete_public_model_catalogs() -> None: models["us"].release_manifest.default_dataset = "unreviewed_us_2025" with pytest.raises(CatalogExtractionError, match="reviewed selection"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -321,6 +389,7 @@ def test_rejects_inconsistent_country_model_identity(identity_source: str) -> No with pytest.raises(CatalogExtractionError, match="identity does not match"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -335,6 +404,7 @@ def test_ignores_only_the_unnamed_structural_parameter_root() -> None: ) catalog = extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -352,6 +422,7 @@ def test_uses_and_validates_public_name_indexed_mappings() -> None: models["uk"].variables_by_name["wrong_name"] = variable with pytest.raises(CatalogExtractionError, match="does not match record name"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -359,33 +430,25 @@ def test_uses_and_validates_public_name_indexed_mappings() -> None: ) -def test_rejects_duplicate_keys_invalid_intervals_and_unknown_region_types() -> None: +def test_rejects_unindexed_mappings_and_invalid_parameter_intervals() -> None: duplicate = source_models() duplicate["us"].parameters_by_name = [] with pytest.raises(CatalogExtractionError, match="name-indexed mapping"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=duplicate, installed_version=installed_version, ) - unknown_region = source_models() - unknown_region["us"].region_registry[1].region_type = "province" - with pytest.raises(CatalogExtractionError, match="unsupported type"): - extract_catalog( - bundle=bundle(), - policyengine_version=POLICYENGINE_VERSION, - models=unknown_region, - installed_version=installed_version, - ) - invalid_interval = source_models() invalid_interval["us"].parameters_by_name["gov.example.rate"].parameter_values[ 0 ].end_date = datetime(2026, 1, 1, tzinfo=UTC) with pytest.raises(CatalogExtractionError, match="canonical inclusive intervals"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=invalid_interval, @@ -399,6 +462,7 @@ def test_rejects_parameter_values_that_are_not_oldest_to_newest() -> None: parameter.parameter_values.reverse() with pytest.raises(CatalogExtractionError, match="oldest to newest"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -419,6 +483,7 @@ def test_collapses_equal_parameter_values_at_the_same_effective_date() -> None: ) catalog = extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -446,6 +511,7 @@ def test_rejects_conflicting_parameter_values_at_the_same_effective_date() -> No with pytest.raises(CatalogExtractionError, match="conflicting values"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -467,6 +533,7 @@ def test_rejects_repeated_parameter_values_with_inconsistent_intervals() -> None with pytest.raises(CatalogExtractionError, match="inconsistent intervals"): extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -491,6 +558,7 @@ def test_preserves_consecutive_day_parameter_value_intervals() -> None: ] catalog = extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, diff --git a/tests/unit/v2/test_catalog_extraction_failures.py b/tests/unit/v2/test_catalog_extraction_failures.py index fa46c3f80..70f5f954c 100644 --- a/tests/unit/v2/test_catalog_extraction_failures.py +++ b/tests/unit/v2/test_catalog_extraction_failures.py @@ -3,9 +3,10 @@ from __future__ import annotations import math +from dataclasses import replace from datetime import date from enum import Enum -from typing import TYPE_CHECKING +from types import SimpleNamespace import pytest @@ -14,19 +15,19 @@ extract_catalog, normalize_json_value, ) +from policyengine_api.data.v2.catalog.region_references import RegionReference from tests.fixtures.v2_catalog import ( POLICYENGINE_VERSION, bundle, installed_version, + region_references, source_models, ) -if TYPE_CHECKING: - from types import SimpleNamespace - def _extract(models: dict[str, SimpleNamespace]) -> None: extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, @@ -46,97 +47,275 @@ def _break_manifest_version(models: dict[str, SimpleNamespace]) -> None: models["us"].release_manifest.policyengine_version = "9.9.9" -def _remove_region_registry(models: dict[str, SimpleNamespace]) -> None: - models["us"].region_registry = None +@pytest.mark.parametrize( + ("break_models", "message"), + [ + ( + _break_package_version, + "^us public model does not match the PolicyEngine.py manifest$", + ), + ( + _break_manifest_country, + "^us public model has an inconsistent release manifest$", + ), + ( + _break_manifest_version, + "^us public model was not certified by the installed PolicyEngine.py " + "version$", + ), + ], +) +def test_inconsistent_public_models_fail_with_a_specific_message( + break_models, + message: str, +) -> None: + models = source_models() + break_models(models) + with pytest.raises(CatalogExtractionError, match=message): + _extract(models) -def _mislabel_region_registry(models: dict[str, SimpleNamespace]) -> None: - models["us"].region_registry.country_id = "uk" +References = dict[str, tuple[RegionReference, ...]] -def _duplicate_region_codes(models: dict[str, SimpleNamespace]) -> None: - registry = models["us"].region_registry - registry.append(registry[1]) + +def _replace_reference( + references: References, country_id: str, code: str, **changes: str | None +) -> None: + references[country_id] = tuple( + replace(reference, **changes) if reference.code == code else reference + for reference in references[country_id] + ) + + +def _point_national_region_at_another_dataset( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["uk"].release_manifest.region_datasets[ + "national" + ].path_template = "frs_2024_25.h5" + models["uk"].release_manifest.datasets["frs_2024_25"] = SimpleNamespace( + path="frs_2024_25.h5", revision="fixture", repo_id=None, repo_type=None + ) + + +def _name_an_uncertified_region_dataset( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.region_datasets["state"].path_template = "missing.h5" + + +def _name_a_per_region_dataset_template( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.region_datasets[ + "state" + ].path_template = "populace_{state_code}.h5" -def _remove_national_region(models: dict[str, SimpleNamespace]) -> None: - registry = models["us"].region_registry - del registry[0] +def _stop_routing_constituencies( + models: dict[str, SimpleNamespace], _references: References +) -> None: + del models["uk"].release_manifest.region_datasets["constituency"] -def _give_national_region_another_dataset(models: dict[str, SimpleNamespace]) -> None: - models["uk"].region_registry[0].dataset_path = "hf://policyengine/uk/other.h5" +def _stop_routing_local_authorities( + models: dict[str, SimpleNamespace], _references: References +) -> None: + del models["uk"].release_manifest.region_datasets["local_authority"] -def _give_filter_an_unsupported_value(models: dict[str, SimpleNamespace]) -> None: - models["us"].region_registry[1].scoping_strategy.variable_value = {"state": "CA"} +def _drop_a_filter_value( + _models: dict[str, SimpleNamespace], references: References +) -> None: + _replace_reference(references, "us", "state/ca", filter_value=None) -def _give_filter_additional_filters(models: dict[str, SimpleNamespace]) -> None: - models["uk"].region_registry[1].scoping_strategy.additional_filters = { - "region": "LONDON" - } +def _give_the_national_region_a_filter_value( + _models: dict[str, SimpleNamespace], references: References +) -> None: + _replace_reference(references, "us", "us", filter_value="0") + + +def _place_a_district_under_a_place( + _models: dict[str, SimpleNamespace], references: References +) -> None: + _replace_reference( + references, + "us", + "congressional_district/CA-01", + parent_code="place/CA-44000", + ) + + +def _remove_a_country_from_the_references( + _models: dict[str, SimpleNamespace], references: References +) -> None: + del references["uk"] + + +def _remove_the_dataset_map( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.datasets = None + + +def _remove_the_reviewed_default_dataset( + models: dict[str, SimpleNamespace], _references: References +) -> None: + del models["us"].release_manifest.datasets["populace_us_2024"] + + +def _remove_the_region_dataset_map( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.region_datasets = None + + +def _remove_a_region_dataset_path( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.region_datasets["state"].path_template = None + + +def _remove_an_artifact_path( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.datasets["populace_us_2024"].path = None + + +def _remove_every_artifact_revision( + models: dict[str, SimpleNamespace], _references: References +) -> None: + manifest = models["us"].release_manifest + manifest.datasets["populace_us_2024"].revision = None + manifest.data_package.release_manifest_revision = None + manifest.data_package.version = None + + +def _drop_the_region_dataset_attribute( + models: dict[str, SimpleNamespace], _references: References +) -> None: + del models["us"].release_manifest.region_datasets + + +def _remove_every_artifact_repository( + models: dict[str, SimpleNamespace], _references: References +) -> None: + models["us"].release_manifest.data_package.repo_id = None @pytest.mark.parametrize( - ("break_models", "message"), + ("break_sources", "message"), [ + (_remove_the_dataset_map, "^us release manifest has no dataset map$"), ( - _break_package_version, - "^us public model does not match the PolicyEngine.py manifest$", + _drop_the_region_dataset_attribute, + "^us release manifest lacks datasets, region_datasets or data_package$", ), ( - _break_manifest_country, - "^us public model has an inconsistent release manifest$", + _remove_the_reviewed_default_dataset, + "^us release does not certify populace_us_2024$", ), ( - _break_manifest_version, - "^us public model was not certified by the installed PolicyEngine.py " - "version$", + _remove_the_region_dataset_map, + "^us release manifest has no region dataset map$", ), - (_remove_region_registry, "^us public region registry is absent$"), - (_mislabel_region_registry, "^us public region registry is absent$"), ( - _duplicate_region_codes, - r"^duplicate region natural keys: \['state/ca'\]$", + _remove_a_region_dataset_path, + "^us state region dataset must be a non-empty string$", ), - (_remove_national_region, "^us national region is absent$"), ( - _give_national_region_another_dataset, + _remove_an_artifact_path, + "^us release artifact populace_us_2024 path must be a non-empty string$", + ), + ( + _remove_every_artifact_revision, + "^us release artifact populace_us_2024 revision must be a non-empty " + "string$", + ), + ( + _remove_every_artifact_repository, + "^us release artifact populace_us_2024 repository must be a non-empty " + "string$", + ), + ( + _point_national_region_at_another_dataset, "^uk national region does not use the reviewed dataset$", ), ( - _give_filter_an_unsupported_value, - "^region 'state/ca' has an unsupported filter value$", + _name_an_uncertified_region_dataset, + "^us release names no certified dataset at path 'missing.h5' for state " + "regions$", + ), + ( + _name_a_per_region_dataset_template, + r"^us state regions name a per-region dataset template " + r"'populace_\{state_code\}.h5', which the catalog does not support$", + ), + ( + _stop_routing_constituencies, + "^the uk release routes no dataset to constituency regions, so region " + "'constituency/E14001063' cannot select its households$", + ), + ( + _stop_routing_local_authorities, + "^the uk release routes no dataset to local_authority regions, so region " + "'local_authority/N09000011' cannot select its households$", + ), + (_drop_a_filter_value, "^region 'state/ca' has no filter value$"), + ( + _give_the_national_region_a_filter_value, + "^region 'us' has a filter value, but a national region selects every " + "household$", + ), + ( + _place_a_district_under_a_place, + "^region 'congressional_district/CA-01' cannot be published because its " + "parent 'place/CA-44000' is left out$", ), ( - _give_filter_additional_filters, - "^region 'country/england' has unsupported additional filters$", + _remove_a_country_from_the_references, + "^region references are absent for: uk$", ), ], ) -def test_inconsistent_public_models_fail_with_a_specific_message( - break_models, +def test_inconsistent_regions_fail_with_a_specific_message( + break_sources, message: str, ) -> None: models = source_models() - break_models(models) + references = region_references() + break_sources(models, references) with pytest.raises(CatalogExtractionError, match=message): - _extract(models) + extract_catalog( + region_references=references, + bundle=bundle(), + policyengine_version=POLICYENGINE_VERSION, + models=models, + installed_version=installed_version, + ) -def test_national_region_without_a_dataset_path_uses_the_reviewed_dataset() -> None: +def test_national_region_without_a_release_dataset_uses_the_reviewed_dataset() -> None: models = source_models() - models["us"].region_registry[0].dataset_path = None + del models["us"].release_manifest.region_datasets["national"] - extract_catalog( + catalog = extract_catalog( + region_references=region_references(), bundle=bundle(), policyengine_version=POLICYENGINE_VERSION, models=models, installed_version=installed_version, ) + us = catalog.country("us") + national = next(region for region in us.regions if region.code == "us") + datasets = {dataset.id: dataset.name for dataset in us.datasets} + assert datasets[national.default_dataset_id] == "populace_us_2024" + assert us.fallback_summaries == () + class _ScalarWrapper: """A numpy-like scalar whose item() returns a plain Python value.""" diff --git a/tests/unit/v2/test_catalog_initialization.py b/tests/unit/v2/test_catalog_initialization.py index e77681fcd..d4d6c22c7 100644 --- a/tests/unit/v2/test_catalog_initialization.py +++ b/tests/unit/v2/test_catalog_initialization.py @@ -8,11 +8,16 @@ import pytest from policyengine_api.data.v2.catalog import initialization +from policyengine_api.data.v2.catalog.errors import CatalogExtractionError from policyengine_api.data.v2.catalog.publication import PublicationEvidence +from policyengine_api.data.v2.catalog.region_data_verification import ( + RegionDataVerificationError, +) from policyengine_api.data.v2.settings import ( V2_DATA_WRITE_DATABASE_URL, V2_SUPABASE_ENVIRONMENT, V2_SUPABASE_PROJECT_REF, + V2ConfigurationError, ) from tests.fixtures.v2_catalog import ( DEPENDENCY_VERSIONS, @@ -65,22 +70,150 @@ def publish(selected_engine, catalog): observed["version"] = catalog.policyengine_version return _evidence() + def verify(selected_engine, catalog, environ): + assert "engine" not in observed + observed["verified"] = ( + selected_engine, + catalog.policyengine_version, + environ, + ) + result = initialization.initialize_catalog( ENVIRONMENT, extractor=normalized_catalog, engine_builder=build, publisher=publish, + region_data_verifier=verify, ) assert result == _evidence() assert observed == { "username": "data-writer", + "verified": (engine, POLICYENGINE_VERSION, ENVIRONMENT), "engine": engine, "version": POLICYENGINE_VERSION, } assert engine.disposed +def test_region_data_failure_publishes_nothing_and_disposes_engine() -> None: + engine = FakeEngine() + + def reject(_engine, _catalog, _environ): + msg = "the certified dataset d has no household column c" + raise RegionDataVerificationError(msg) + + def refuse_publication(_engine, _catalog): + msg = "publication must not start" + raise AssertionError(msg) + + with pytest.raises(RegionDataVerificationError, match="no household column"): + initialization.initialize_catalog( + ENVIRONMENT, + extractor=normalized_catalog, + engine_builder=lambda _settings: engine, + publisher=refuse_publication, + region_data_verifier=reject, + ) + assert engine.disposed + + +def test_installed_verification_reads_artifacts_with_the_hugging_face_token( + monkeypatch: pytest.MonkeyPatch, +) -> None: + observed = {} + + class RecordingInspector: + def __init__(self, *, token): + observed["token"] = token + + def record(catalog, inspector, *, countries): + observed["call"] = (catalog.policyengine_version, type(inspector), countries) + + monkeypatch.setattr(initialization, "HuggingFaceDataInspector", RecordingInspector) + monkeypatch.setattr(initialization, "verify_region_data", record) + + initialization.verify_installed_region_data( + normalized_catalog(), ("uk",), {"HUGGING_FACE_TOKEN": "read-token"} + ) + assert observed == { + "token": "read-token", + "call": (POLICYENGINE_VERSION, RecordingInspector, ("uk",)), + } + + initialization.verify_installed_region_data(normalized_catalog(), ("us",), {}) + assert observed == { + "token": None, + "call": (POLICYENGINE_VERSION, RecordingInspector, ("us",)), + } + + +@pytest.mark.parametrize("environ", [{}, {"HUGGING_FACE_TOKEN": ""}]) +def test_uk_verification_requires_the_hugging_face_token_before_reading( + monkeypatch: pytest.MonkeyPatch, + environ: dict[str, str], +) -> None: + def refuse(*_args, **_kwargs): + msg = "no artifact may be read without the token" + raise AssertionError(msg) + + monkeypatch.setattr(initialization, "HuggingFaceDataInspector", refuse) + monkeypatch.setattr(initialization, "verify_region_data", refuse) + + with pytest.raises( + V2ConfigurationError, + match=r"^HUGGING_FACE_TOKEN must be set to read the certified uk data, " + r"whose repository is private$", + ): + initialization.verify_installed_region_data( + normalized_catalog(), ("us", "uk"), environ + ) + + +def test_default_verification_checks_only_countries_not_yet_published( + monkeypatch: pytest.MonkeyPatch, +) -> None: + engine = FakeEngine() + catalog = normalized_catalog() + observed = {} + + def find_unpublished(selected_engine, selected_catalog): + observed["found"] = (selected_engine, selected_catalog) + return ("uk",) + + def verify(selected_catalog, countries, environ): + observed["verified"] = (selected_catalog, countries, environ) + + monkeypatch.setattr(initialization, "unpublished_countries", find_unpublished) + monkeypatch.setattr(initialization, "verify_installed_region_data", verify) + + initialization.verify_unpublished_region_data(engine, catalog, ENVIRONMENT) + + assert observed == { + "found": (engine, catalog), + "verified": (catalog, ("uk",), ENVIRONMENT), + } + + +def test_main_reports_a_region_data_failure_with_its_message( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + def fail(): + msg = ( + "the certified dataset d has no household column c, which 1 uk region " + "filters on (constituency/x)" + ) + raise RegionDataVerificationError(msg) + + monkeypatch.setattr(initialization, "initialize_catalog", fail) + + assert initialization.main() == 1 + failure = capsys.readouterr() + assert '"type": "RegionDataVerificationError"' in failure.err + assert "which 1 uk region filters on" in failure.err + + def test_extraction_failure_opens_no_engine() -> None: def fail_extraction(): msg = "extraction failed" @@ -142,3 +275,27 @@ def test_initializer_is_not_imported_by_application_startup_modules() -> None: module.startswith("policyengine_api.data.v2.catalog.initialization") for module in imported ) + + +def test_region_data_errors_are_not_reported_as_extraction_errors() -> None: + # Extraction errors are raised before any database access; verification + # runs after reading the stored versions and reads certified data over the + # network. + assert not issubclass(RegionDataVerificationError, CatalogExtractionError) + + +def test_main_reports_a_missing_hugging_face_token_with_its_message( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + def verify_without_token(): + initialization.verify_installed_region_data(normalized_catalog(), ("uk",), {}) + + monkeypatch.setattr(initialization, "initialize_catalog", verify_without_token) + + assert initialization.main() == 1 + failure = capsys.readouterr() + assert '"type": "V2ConfigurationError"' in failure.err + assert "HUGGING_FACE_TOKEN must be set to read the certified uk data" in ( + failure.err + ) diff --git a/tests/unit/v2/test_catalog_publication.py b/tests/unit/v2/test_catalog_publication.py index a3cbe0d65..2341003dc 100644 --- a/tests/unit/v2/test_catalog_publication.py +++ b/tests/unit/v2/test_catalog_publication.py @@ -2,6 +2,7 @@ from __future__ import annotations +import logging from io import StringIO from pathlib import Path from unittest.mock import patch @@ -214,10 +215,20 @@ def test_publication_uses_mapped_tables_and_sqlalchemy_statements() -> None: catalog = normalized_catalog() comparisons = publication_reconciliation._comparison_pairs(catalog.country("us")) + assert [table_name for table_name, _staged, _stored in comparisons] == [ + "model version", + "variables", + "parameter nodes", + "parameters", + "parameter values", + "datasets", + ] + statements = [ + statement for _table_name, *pair in comparisons for statement in pair + ] + list(publication_reconciliation._region_comparison_pair(catalog.country("us"))) assert all( isinstance(statement, sa.sql.Select) and not isinstance(statement, TextClause) - for pair in comparisons - for statement in pair + for statement in statements ) class Result: @@ -251,6 +262,8 @@ def test_completion_evidence_contains_only_reviewed_non_secret_fields() -> None: entity_counts={"parameters": 12}, fallback_summaries=(("us", "state", 3),), elapsed_seconds=2.3456, + omitted_region_summaries=(("us", "place", 333),), + kept_region_summaries=(("uk", "6.7.5", 5, 1_015),), ).as_dict() assert evidence == { @@ -261,6 +274,17 @@ def test_completion_evidence_contains_only_reviewed_non_secret_fields() -> None: "fallback_summaries": [ {"country_id": "us", "region_type": "state", "count": 3} ], + "omitted_region_summaries": [ + {"country_id": "us", "region_type": "place", "count": 333} + ], + "kept_regions": [ + { + "country_id": "uk", + "policyengine_version": "6.7.5", + "stored_regions": 5, + "staged_regions": 1_015, + } + ], "elapsed_seconds": 2.346, } assert ( @@ -293,3 +317,55 @@ def test_fallback_summary_emits_one_non_secret_warning() -> None: rendered_message = message_template % (logged_summaries,) assert "regional dataset fallback summary" in rendered_message assert "postgresql://" not in rendered_message + + +def _kept_region_records( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, + differences: tuple[int, int], +) -> list[logging.LogRecord]: + monkeypatch.setattr( + publication, + "count_region_differences", + lambda _connection, _country: differences, + ) + monkeypatch.setattr(publication, "count_stored_regions", lambda *_args: 4) + # An earlier test's Alembic logging setup may have disabled existing loggers. + monkeypatch.setattr(publication.LOGGER, "disabled", False) + us = normalized_catalog().country("us") + with caplog.at_level(logging.WARNING, logger=publication.LOGGER.name): + summary = publication._kept_region_summary(object(), us) + assert summary == (None if differences == (0, 0) else ("us", "5.0.4", 4, 3)) + return [ + record for record in caplog.records if record.name == publication.LOGGER.name + ] + + +def test_kept_region_warning_prints_country_version_and_counts_without_logging_setup( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + [record] = _kept_region_records(monkeypatch, caplog, (1, 2)) + + # The seed command configures no logging, so Python's last-resort handler + # prints the record; this is the text the job output shows. + assert logging.lastResort is not None + assert logging.lastResort.format(record) == ( + "kept the published regions of the stored us catalog version 5.0.4: " + "1 staged region rows are not stored and 2 stored region rows are not " + "staged" + ) + assert record.levelno == logging.WARNING + assert record.policyengine_attributes == { + "country_id": "us", + "policyengine_version": "5.0.4", + "staged_region_rows_not_stored": 1, + "stored_region_rows_not_staged": 2, + } + + +def test_matching_kept_regions_log_nothing( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +) -> None: + assert _kept_region_records(monkeypatch, caplog, (0, 0)) == [] diff --git a/tests/unit/v2/test_certified_data_inspection.py b/tests/unit/v2/test_certified_data_inspection.py new file mode 100644 index 000000000..a0b375909 --- /dev/null +++ b/tests/unit/v2/test_certified_data_inspection.py @@ -0,0 +1,234 @@ +"""Reading household column names from certified HDF5 datasets.""" + +from __future__ import annotations + +import ast +import importlib.metadata +import tomllib +from pathlib import Path + +import h5py +import numpy as np +import pandas as pd +import pytest +from huggingface_hub import HfFileSystem +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +from policyengine_api.data.v2.catalog.certified_data_inspection import ( + CertifiedDataReadError, + HuggingFaceDataInspector, + household_columns_in_hdf5, + huggingface_path, +) +from policyengine_api.data.v2.catalog.records import CertifiedArtifact + +HOUSEHOLDS = pd.DataFrame( + { + "household_id": [1, 2], + "household_weight": [1.5, 2.5], + "state_fips": [6, 36], + } +) + + +def _artifact( + *, + name: str = "populace_us_2024_acs_local", + repo_type: str = "dataset", + revision: str = "populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z", +) -> CertifiedArtifact: + return CertifiedArtifact( + name=name, + repo_id="policyengine/populace-us", + repo_type=repo_type, + path=f"{name}.h5", + revision=revision, + sha256=None, + ) + + +def _columns(path: Path) -> frozenset[str]: + with h5py.File(path, "r") as h5: + return household_columns_in_hdf5(h5) + + +def test_reads_columns_of_a_fixed_format_household_frame(tmp_path: Path) -> None: + path = tmp_path / "fixed.h5" + HOUSEHOLDS.to_hdf(path, key="household", format="fixed") + + assert _columns(path) == {"household_id", "household_weight", "state_fips"} + + +def test_reads_data_columns_of_a_table_format_household_frame(tmp_path: Path) -> None: + path = tmp_path / "table.h5" + frame = HOUSEHOLDS.assign(region=pd.Categorical(["LONDON", "WALES"])) + frame.to_hdf(path, key="household", format="table", data_columns=True) + + assert _columns(path) == { + "household_id", + "household_weight", + "state_fips", + "region", + } + + +# PyTables warns that year keys are not Python identifiers; policyengine-uk's +# multi-year files use the same keys. +@pytest.mark.filterwarnings("ignore:object name is not a valid Python identifier") +def test_keeps_only_columns_every_year_of_a_multi_year_file_has( + tmp_path: Path, +) -> None: + path = tmp_path / "years.h5" + HOUSEHOLDS.to_hdf(path, key="household/2024", format="table", data_columns=True) + HOUSEHOLDS.drop(columns="state_fips").to_hdf( + path, key="household/2025", format="table", data_columns=True + ) + + assert _columns(path) == {"household_id", "household_weight"} + + +def test_reads_variable_names_of_a_core_format_file(tmp_path: Path) -> None: + path = tmp_path / "core.h5" + with h5py.File(path, "w") as h5: + h5.create_dataset("state_fips/2024", data=np.array([6, 36])) + h5.create_dataset("household_weight/2024", data=np.array([1.5, 2.5])) + + assert _columns(path) == {"state_fips", "household_weight"} + + +def test_refuses_a_table_whose_columns_are_stored_in_value_blocks( + tmp_path: Path, +) -> None: + path = tmp_path / "blocks.h5" + HOUSEHOLDS.to_hdf(path, key="household", format="table") + + with pytest.raises( + CertifiedDataReadError, + match=r"^household table household stores columns in value blocks, whose " + r"names cannot be read without unpickling$", + ): + _columns(path) + + +def test_refuses_a_household_group_that_holds_no_frame(tmp_path: Path) -> None: + path = tmp_path / "empty.h5" + with h5py.File(path, "w") as h5: + h5.create_group("household") + + with pytest.raises( + CertifiedDataReadError, match=r"^household group holds no data frame$" + ): + _columns(path) + + +def test_refuses_a_household_entry_that_is_not_a_group(tmp_path: Path) -> None: + path = tmp_path / "flat.h5" + with h5py.File(path, "w") as h5: + h5.create_dataset("household", data=np.array([1, 2])) + + with pytest.raises( + CertifiedDataReadError, match=r"^household entry is not an HDF5 group$" + ): + _columns(path) + + +def test_default_file_system_is_hugging_face_with_the_token() -> None: + inspector = HuggingFaceDataInspector(token="read-token") + + file_system = inspector._file_system + assert isinstance(file_system, HfFileSystem) + assert file_system.token == "read-token" + + +def test_names_dataset_and_model_repository_files_at_their_revision() -> None: + assert huggingface_path(_artifact()) == ( + "datasets/policyengine/populace-us@" + "populace-us-2024-buildo-acs-local-766b7c7-20261017T030317Z/" + "populace_us_2024_acs_local.h5" + ) + assert huggingface_path( + _artifact(name="microcosm_uk_2024_25", repo_type="model", revision="1.0.0") + ) == ("policyengine/populace-us@1.0.0/microcosm_uk_2024_25.h5") + assert huggingface_path(_artifact(revision="refs/pr/1")).startswith( + "datasets/policyengine/populace-us@refs%2Fpr%2F1/" + ) + + +class FakeFileSystem: + """Serves local files for Hugging Face paths and records how it was built.""" + + def __init__(self, files: dict[str, Path]) -> None: + self.files = files + self.opened: list[str] = [] + + def open(self, path: str, mode: str, *, block_size: int, cache_type: str): + assert (mode, block_size, cache_type) == ("rb", 1 << 20, "blockcache") + self.opened.append(path) + return self.files[path].open("rb") + + +def test_inspector_reads_artifacts_through_an_authenticated_file_system( + tmp_path: Path, +) -> None: + dataset_path = tmp_path / "dataset.h5" + HOUSEHOLDS.to_hdf(dataset_path, key="household", format="fixed") + local_area_path = tmp_path / "local_area.h5" + HOUSEHOLDS.assign(la_code_oa=["N09000011", "E07000110"]).to_hdf( + local_area_path, key="household", format="table", data_columns=True + ) + dataset = _artifact() + local_area = _artifact(name="microcosm_uk_2024_25_dense", repo_type="dataset") + file_system = FakeFileSystem( + { + huggingface_path(dataset): dataset_path, + huggingface_path(local_area): local_area_path, + } + ) + tokens: list[str | None] = [] + + def build_file_system(token: str | None) -> FakeFileSystem: + tokens.append(token) + return file_system + + inspector = HuggingFaceDataInspector( + token="read-token", file_system_factory=build_file_system + ) + + assert "state_fips" in inspector.household_columns(dataset) + assert "la_code_oa" in inspector.household_columns(local_area) + assert tokens == ["read-token"] + assert file_system.opened == [ + huggingface_path(dataset), + huggingface_path(local_area), + ] + + +def test_imports_only_packages_the_project_depends_on_directly() -> None: + repo = Path(__file__).parents[3] + module = repo / "policyengine_api/data/v2/catalog/certified_data_inspection.py" + tree = ast.parse(module.read_text(encoding="utf-8")) + top_level_imports = { + alias.name.split(".")[0] + for node in ast.walk(tree) + if isinstance(node, ast.Import) + for alias in node.names + } | { + (node.module or "").split(".")[0] + for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.level == 0 + } + distributions = importlib.metadata.packages_distributions() + third_party = { + canonicalize_name(distributions[name][0]) + for name in top_level_imports + if name in distributions and name != "policyengine_api" + } + project = tomllib.loads((repo / "pyproject.toml").read_text(encoding="utf-8")) + direct = { + canonicalize_name(Requirement(requirement).name) + for requirement in project["project"]["dependencies"] + } + + assert third_party == {"h5py", "huggingface-hub"} + assert third_party <= direct diff --git a/tests/unit/v2/test_metadata_deployment.py b/tests/unit/v2/test_metadata_deployment.py index c752b46c0..a5e51e864 100644 --- a/tests/unit/v2/test_metadata_deployment.py +++ b/tests/unit/v2/test_metadata_deployment.py @@ -171,3 +171,12 @@ def test_stage_9_deployment_shell_script_is_syntax_valid() -> None: ) assert result.returncode == 0, result.stderr + + +def test_only_catalog_publication_receives_the_hugging_face_read_token() -> None: + workflow = _read(".github/workflows/seed-v2-database.yml") + publication = _step(workflow, "Seed and validate the v2 metadata catalog") + token = "HUGGING_FACE_TOKEN: ${{ secrets.PE_UK_PRIVATE_HF_READ_TOKEN }}" + + assert token in publication + assert workflow.count("HUGGING_FACE_TOKEN") == 1 diff --git a/tests/unit/v2/test_region_data_verification.py b/tests/unit/v2/test_region_data_verification.py new file mode 100644 index 000000000..11c719a41 --- /dev/null +++ b/tests/unit/v2/test_region_data_verification.py @@ -0,0 +1,186 @@ +"""Publication refuses regions the certified data cannot select.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +import pytest + +from policyengine_api.data.v2.catalog.errors import CertifiedDataReadError +from policyengine_api.data.v2.catalog.region_data_verification import ( + RegionDataVerificationError, + verify_region_data, +) +from tests.fixtures.v2_catalog import normalized_catalog + +if TYPE_CHECKING: + from policyengine_api.data.v2.catalog.records import CertifiedArtifact + +ALL_COLUMNS = { + "populace_us_2024_acs_local": frozenset( + {"state_fips", "congressional_district_geoid", "household_weight"} + ), + "microcosm_uk_2024_25": frozenset({"region", "household_weight"}), + "microcosm_uk_2024_25_dense": frozenset( + {"constituency_code_oa", "la_code_oa", "household_weight"} + ), +} + + +class FakeInspector: + """Answers from fixed columns, and records each artifact read.""" + + def __init__( + self, + *, + columns: dict[str, frozenset[str]] | None = None, + failure: Exception | None = None, + ) -> None: + self.columns = ALL_COLUMNS if columns is None else columns + self.failure = failure + self.reads: list[str] = [] + + def household_columns(self, artifact: CertifiedArtifact) -> frozenset[str]: + self.reads.append(artifact.name) + if self.failure is not None: + raise self.failure + return self.columns[artifact.name] + + +def _verify( + inspector: FakeInspector, + *, + countries: tuple[str, ...] = ("us", "uk"), +) -> None: + verify_region_data(normalized_catalog(), inspector, countries=countries) + + +def test_catalog_records_only_the_artifacts_its_filters_read() -> None: + catalog = normalized_catalog() + + assert [ + (artifact.name, artifact.repo_id, artifact.repo_type, artifact.revision) + for artifact in catalog.country("us").certified_artifacts + ] == [ + ( + "populace_us_2024_acs_local", + "policyengine/populace-us", + "dataset", + "fixture", + ) + ] + assert [ + (artifact.name, artifact.repo_id, artifact.path, artifact.repo_type) + for artifact in catalog.country("uk").certified_artifacts + ] == [ + ( + "microcosm_uk_2024_25", + "example-org/example-uk-data", + "microcosm_uk_2024_25.h5", + "model", + ), + ( + "microcosm_uk_2024_25_dense", + "example-org/example-uk-data", + "microcosm_uk_2024_25_dense.h5", + "model", + ), + ] + + +def test_accepts_regions_whose_filter_columns_exist() -> None: + inspector = FakeInspector() + + _verify(inspector) + + assert sorted(inspector.reads) == [ + "microcosm_uk_2024_25", + "microcosm_uk_2024_25_dense", + "populace_us_2024_acs_local", + ] + + +def test_reads_only_the_artifacts_of_the_countries_it_verifies() -> None: + inspector = FakeInspector() + + _verify(inspector, countries=("us",)) + + assert inspector.reads == ["populace_us_2024_acs_local"] + + +def test_verifies_nothing_when_every_version_is_already_published() -> None: + inspector = FakeInspector(failure=AssertionError("must not read")) + + _verify(inspector, countries=()) + + assert inspector.reads == [] + + +def test_rejects_a_filter_column_missing_from_the_dataset() -> None: + inspector = FakeInspector( + columns={ + **ALL_COLUMNS, + "populace_us_2024_acs_local": frozenset({"state_fips"}), + } + ) + + with pytest.raises( + RegionDataVerificationError, + match=r"^the certified dataset populace_us_2024_acs_local has no household " + r"column congressional_district_geoid, which 1 us region filters on " + r"\(congressional_district/CA-01\)$", + ): + _verify(inspector) + + +def test_rejects_a_local_area_dataset_without_the_area_code_column() -> None: + inspector = FakeInspector( + columns={ + **ALL_COLUMNS, + "microcosm_uk_2024_25_dense": frozenset({"la_code_oa"}), + } + ) + + with pytest.raises( + RegionDataVerificationError, + match=r"^the certified dataset microcosm_uk_2024_25_dense has no household " + r"column constituency_code_oa, which 1 uk region filters on " + r"\(constituency/E14001063\)$", + ): + _verify(inspector, countries=("uk",)) + + +def test_reports_an_unreadable_artifact_without_its_error_text() -> None: + inspector = FakeInspector( + failure=PermissionError("403 for https://example.test/?token=do-not-print") + ) + + with pytest.raises(RegionDataVerificationError) as raised: + _verify(inspector, countries=("us",)) + + assert str(raised.value) == ( + "could not read the certified artifact populace_us_2024_acs_local: " + "PermissionError" + ) + assert "do-not-print" not in str(raised.value) + assert "fixture" not in str(raised.value) + + +def test_reports_an_artifact_structure_error_with_its_own_message() -> None: + inspector = FakeInspector( + failure=CertifiedDataReadError("household group holds no data frame") + ) + + with pytest.raises( + RegionDataVerificationError, + match=r"^the certified artifact populace_us_2024_acs_local cannot be " + r"checked: household group holds no data frame$", + ): + _verify(inspector, countries=("us",)) + + +def test_passes_on_a_verification_error_the_inspector_raises() -> None: + inspector = FakeInspector(failure=RegionDataVerificationError("already explained")) + + with pytest.raises(RegionDataVerificationError, match=r"^already explained$"): + _verify(inspector, countries=("us",)) diff --git a/tests/unit/v2/test_region_references.py b/tests/unit/v2/test_region_references.py new file mode 100644 index 000000000..e2493a2d5 --- /dev/null +++ b/tests/unit/v2/test_region_references.py @@ -0,0 +1,353 @@ +"""The checked-in region reference files and the rules their loader enforces.""" + +from __future__ import annotations + +import csv +import json +from collections import Counter +from pathlib import Path + +import pytest + +from policyengine_api.data.congressional_districts import ( + AT_LARGE_STATES, + CONGRESSIONAL_DISTRICTS, + STATE_CODE_TO_NAME, + build_congressional_district_metadata, +) +from policyengine_api.data.v2.catalog.errors import CatalogExtractionError +from policyengine_api.data.v2.catalog.region_references import ( + REGION_REFERENCE_FIELDS, + RegionReference, + load_region_references, + parse_region_references, +) + +V1_DATA_DIRECTORY = Path(__file__).parents[3] / "policyengine_api" / "data" +NATION_BY_ONS_PREFIX = { + "E": "country/england", + "S": "country/scotland", + "W": "country/wales", + "N": "country/northern_ireland", +} +ENGLISH_ITL1_REGIONS = [ + "NORTH_EAST", + "NORTH_WEST", + "YORKSHIRE", + "EAST_MIDLANDS", + "WEST_MIDLANDS", + "EAST_OF_ENGLAND", + "LONDON", + "SOUTH_EAST", + "SOUTH_WEST", +] + + +def _by_code(references: tuple[RegionReference, ...]) -> dict[str, RegionReference]: + return {reference.code: reference for reference in references} + + +def _v1_lookup_rows(filename: str) -> list[dict[str, str]]: + with (V1_DATA_DIRECTORY / filename).open(encoding="utf-8", newline="") as handle: + return list(csv.DictReader(handle)) + + +def _row(code: str, region_type: str, **fields: str) -> dict[str, str]: + """Return one reference-file row; unspecified columns are empty.""" + row = dict.fromkeys(REGION_REFERENCE_FIELDS, "") + row.update(code=code, label=code.title(), region_type=region_type, v1_code=code) + row.update(fields) + return row + + +def _small_us_rows() -> list[dict[str, str]]: + return [ + _row("us", "national"), + _row("state/ca", "state", parent_code="us", filter_value="6"), + _row( + "congressional_district/CA-01", + "congressional_district", + parent_code="state/ca", + ), + ] + + +def test_each_country_lists_every_region_once_with_the_expected_counts() -> None: + us = load_region_references("us") + uk = load_region_references("uk") + + assert Counter(reference.region_type for reference in us) == { + "national": 1, + "state": 51, + "congressional_district": 436, + "place": 333, + } + assert Counter(reference.region_type for reference in uk) == { + "national": 1, + "country": 4, + "constituency": 650, + "local_authority": 361, + } + assert len(uk) == 1_016 + for references in (us, uk): + codes = [reference.code for reference in references] + v1_codes = [reference.v1_code for reference in references] + assert len(set(codes)) == len(codes) + assert len(set(v1_codes)) == len(v1_codes) + + +def test_national_regions_and_the_v1_identifiers_they_replace() -> None: + us = _by_code(load_region_references("us")) + uk = _by_code(load_region_references("uk")) + + assert us["us"].label == "United States" + assert uk["uk"].label == "United Kingdom" + assert us["us"].parent_code is None + assert uk["uk"].parent_code is None + assert uk["country/northern_ireland"].v1_code == "country/ni" + assert uk["country/northern_ireland"].label == "Northern Ireland" + assert "country/ni" not in uk + assert uk["constituency/E14001063"].label == "Aldershot" + assert uk["constituency/E14001063"].v1_code == "constituency/Aldershot" + assert uk["local_authority/E07000110"].label == "Maidstone" + assert uk["local_authority/E07000110"].v1_code == "local_authority/Maidstone" + assert uk["local_authority/N09000011"].label == "Ards and North Down" + assert uk["local_authority/N09000011"].parent_code == "country/northern_ireland" + assert uk["local_authority/N09000011"].v1_code == ( + "local_authority/Ards and North Down" + ) + assert us["state/ca"].v1_code == "state/ca" + assert us["congressional_district/CA-01"].v1_code == "congressional_district/CA-01" + + +def test_uk_constituencies_and_local_authorities_use_ons_codes_from_v1_lookups() -> ( + None +): + uk = load_region_references("uk") + for region_type, filename in ( + ("constituency", "constituencies_2024.csv"), + ("local_authority", "local_authorities_2021.csv"), + ): + expected = { + f"{region_type}/{row['code']}": ( + row["name"], + f"{region_type}/{row['name']}", + row["code"], + NATION_BY_ONS_PREFIX[row["code"][0]], + ) + for row in _v1_lookup_rows(filename) + } + actual = { + reference.code: ( + reference.label, + reference.v1_code, + reference.filter_value, + reference.parent_code, + ) + for reference in uk + if reference.region_type == region_type + } + assert actual == expected + + +def test_uk_nations_select_their_stored_regions() -> None: + uk = _by_code(load_region_references("uk")) + + assert json.loads(uk["country/england"].filter_value or "") == ( + ENGLISH_ITL1_REGIONS + ) + assert uk["country/scotland"].filter_value == "SCOTLAND" + assert uk["country/wales"].filter_value == "WALES" + assert uk["country/northern_ireland"].filter_value == "NORTHERN_IRELAND" + assert all(uk[code].parent_code == "uk" for code in NATION_BY_ONS_PREFIX.values()) + + +def test_us_states_and_districts_match_v1_and_select_by_fips_and_geoid() -> None: + us = load_region_references("us") + states = { + reference.state_code: reference + for reference in us + if reference.region_type == "state" + } + districts = { + reference.code: reference + for reference in us + if reference.region_type == "congressional_district" + } + + assert {code: state.label for code, state in states.items()} == (STATE_CODE_TO_NAME) + assert all(state.code == f"state/{code.lower()}" for code, state in states.items()) + assert all(state.parent_code == "us" for state in states.values()) + assert states["CA"].filter_value == "6" + assert states["DC"].filter_value == "11" + + v1_districts = { + entry["name"]: entry for entry in build_congressional_district_metadata() + } + assert set(districts) == set(v1_districts) + for district in CONGRESSIONAL_DISTRICTS: + code = f"congressional_district/{district.state_code}-{district.number:02d}" + reference = districts[code] + number = 0 if district.state_code in AT_LARGE_STATES else district.number + state_fips = int(states[district.state_code].filter_value or "") + assert reference.label == v1_districts[code]["label"] + assert reference.parent_code == f"state/{district.state_code.lower()}" + assert reference.state_code == district.state_code + assert reference.state_name == STATE_CODE_TO_NAME[district.state_code] + assert reference.filter_value == str(state_fips * 100 + number) + assert districts["congressional_district/CA-01"].filter_value == "601" + assert districts["congressional_district/AK-01"].filter_value == "200" + assert districts["congressional_district/DC-01"].filter_value == "1100" + + +def test_us_places_are_listed_under_their_state_without_a_filter() -> None: + places = [ + reference + for reference in load_region_references("us") + if reference.region_type == "place" + ] + + assert places + assert all(place.filter_value is None for place in places) + assert all( + place.parent_code == f"state/{(place.state_code or '').lower()}" + for place in places + ) + assert all(place.v1_code == place.code for place in places) + + +def test_parses_a_valid_minimal_file() -> None: + references = parse_region_references("us", _small_us_rows()) + + assert [reference.code for reference in references] == [ + "us", + "state/ca", + "congressional_district/CA-01", + ] + assert references[1] == RegionReference( + code="state/ca", + label="State/Ca", + region_type="state", + parent_code="us", + state_code=None, + state_name=None, + filter_value="6", + v1_code="state/ca", + ) + + +@pytest.mark.parametrize( + ("change", "message"), + [ + ( + lambda rows: rows.append(_row("state/ca", "state", parent_code="us")), + r"^us region reference file repeats region codes: \['state/ca'\]$", + ), + ( + lambda rows: rows[2].update(v1_code="state/ca"), + r"^us region reference file repeats v1 identifiers: \['state/ca'\]$", + ), + ( + lambda rows: rows[2].update(parent_code="state/ny"), + r"^region 'congressional_district/CA-01' names an unknown parent " + r"'state/ny'$", + ), + ( + lambda rows: rows.pop(0), + r"^us region reference file has no national region 'us'$", + ), + ( + lambda rows: rows[1].update(region_type="place"), + r"^region 'state/ca' does not start with its type 'place/'$", + ), + ( + lambda rows: rows.append(_row("county/06037", "county", parent_code="us")), + r"^region 'county/06037' has unsupported type 'county'$", + ), + ( + lambda rows: rows[1].update(parent_code="congressional_district/CA-01"), + r"^region 'state/ca' has a parent cycle$", + ), + ( + lambda rows: rows[0].update(parent_code="state/ca"), + r"^national region 'us' must have no parent$", + ), + ( + lambda rows: rows[1].update(parent_code=""), + r"^region 'state/ca' has no parent$", + ), + ( + lambda rows: rows[1].update(filter_value="x" * 256), + r"^region 'state/ca' filter_value exceeds 255 characters$", + ), + ( + lambda rows: rows[1].update(label=" California"), + r"^region 'state/ca' label has surrounding whitespace$", + ), + ( + lambda rows: rows[1].update(label=""), + r"^region 'state/ca' label must be a non-empty string$", + ), + ( + lambda rows: rows[1].update(code=""), + r"^us region reference file has an invalid code ''$", + ), + ( + lambda rows: rows[1].update(code="state/ca "), + r"^us region reference file has an invalid code 'state/ca '$", + ), + ( + lambda rows: rows[1].update(code="state/" + "c" * 250), + r"^us region reference file has an invalid code 'state/c+'$", + ), + ( + lambda rows: rows.append(_row("usa", "national")), + r"^national region 'usa' must be named 'us'$", + ), + ], +) +def test_rejects_an_inconsistent_reference_file(change, message: str) -> None: + rows = _small_us_rows() + change(rows) + + with pytest.raises(CatalogExtractionError, match=message): + parse_region_references("us", rows) + + +def test_names_the_missing_ancestor_further_up_a_regions_chain() -> None: + rows = [ + _row("us", "national"), + _row( + "congressional_district/CA-01", + "congressional_district", + parent_code="state/ca", + ), + _row("state/ca", "state", parent_code="state/zz"), + ] + + with pytest.raises( + CatalogExtractionError, + match=r"^region 'congressional_district/CA-01' descends from 'state/ca', " + r"whose parent 'state/zz' is unknown$", + ): + parse_region_references("us", rows) + + +def test_rejects_a_file_whose_columns_differ(tmp_path: Path) -> None: + (tmp_path / "us.csv").write_text( + "code,label,region_type\nus,United States,national\n", encoding="utf-8" + ) + + with pytest.raises( + CatalogExtractionError, + match=r"^us region reference file columns must be ", + ): + load_region_references("us", directory=tmp_path) + + +def test_rejects_a_country_without_a_reference_file(tmp_path: Path) -> None: + with pytest.raises( + CatalogExtractionError, + match=r"^ca has no region reference file$", + ): + load_region_references("ca", directory=tmp_path) diff --git a/uv.lock b/uv.lock index 094aec459..14d4b7346 100644 --- a/uv.lock +++ b/uv.lock @@ -2786,7 +2786,9 @@ dependencies = [ { name = "google-cloud-logging" }, { name = "google-cloud-secret-manager" }, { name = "gunicorn" }, + { name = "h5py" }, { name = "httpx" }, + { name = "huggingface-hub" }, { name = "markupsafe" }, { name = "microdf-python" }, { name = "openai" }, @@ -2840,7 +2842,9 @@ requires-dist = [ { name = "google-cloud-logging", specifier = ">=3,<4" }, { name = "google-cloud-secret-manager", specifier = ">=2,<3" }, { name = "gunicorn" }, + { name = "h5py", specifier = ">=3.16,<4" }, { name = "httpx", specifier = ">=0.27.0" }, + { name = "huggingface-hub", specifier = ">=1.12,<2" }, { name = "markupsafe", specifier = ">=3,<4" }, { name = "microdf-python", specifier = ">=1.0.0" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15,<2" },