From c66f0708b3814e4730f22cb65adc4546587435f1 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:19:22 -0400 Subject: [PATCH 1/2] Avoid county FIPS network download --- .gitignore | 1 + changelog.d/fixed/8650.md | 1 + policyengine_us/data/county_fips_2020.csv.gz | Bin 0 -> 32315 bytes .../test_load_county_fips_dataset.py | 108 +++++------------- .../tools/geography/county_helpers.py | 62 +++++----- 5 files changed, 59 insertions(+), 113 deletions(-) create mode 100644 changelog.d/fixed/8650.md create mode 100644 policyengine_us/data/county_fips_2020.csv.gz diff --git a/.gitignore b/.gitignore index e9e814be967..d0c9b065baf 100644 --- a/.gitignore +++ b/.gitignore @@ -61,6 +61,7 @@ venv/** oryx-build-commands.txt **/*.csv.gz +!policyengine_us/data/county_fips_2020.csv.gz **/*.pkl # Claude Code temporary files diff --git a/changelog.d/fixed/8650.md b/changelog.d/fixed/8650.md new file mode 100644 index 00000000000..01c6df7ff60 --- /dev/null +++ b/changelog.d/fixed/8650.md @@ -0,0 +1 @@ +Bundle the county FIPS dataset so county lookups do not require live Hugging Face access. diff --git a/policyengine_us/data/county_fips_2020.csv.gz b/policyengine_us/data/county_fips_2020.csv.gz new file mode 100644 index 0000000000000000000000000000000000000000..b8cd43c62ffd19e8e6142209253cd6d0b80792c6 GIT binary patch literal 32315 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 I9pK*m4*>R9w*UYD literal 0 HcmV?d00001 diff --git a/policyengine_us/tests/utilities/test_load_county_fips_dataset.py b/policyengine_us/tests/utilities/test_load_county_fips_dataset.py index 4b4620f0204..102f18b7f05 100644 --- a/policyengine_us/tests/utilities/test_load_county_fips_dataset.py +++ b/policyengine_us/tests/utilities/test_load_county_fips_dataset.py @@ -1,11 +1,14 @@ -from policyengine_core.tools.hugging_face import download_huggingface_dataset +import gzip +from pathlib import Path + +import pandas as pd +import pytest + +from policyengine_us.tools.geography import county_helpers from policyengine_us.tools.geography.county_helpers import ( + COUNTY_FIPS_DATASET_FILENAME, load_county_fips_dataset, ) -from pathlib import Path -import pytest -import pandas as pd -import gzip @pytest.fixture @@ -33,114 +36,57 @@ def mock_dataset_file(tmp_fips_dir) -> Path: ) # Save as gzipped CSV - test_file_path = tmp_fips_dir / "county_fips_2020.csv.gz" + test_file_path = tmp_fips_dir / COUNTY_FIPS_DATASET_FILENAME with gzip.open(test_file_path, "wb") as f: test_data.to_csv(f, index=False, encoding="utf-8") return test_file_path -def mock_download_huggingface_dataset_success(filepath): - def _mock(*args, **kwargs): - return filepath - - return _mock - - -def mock_download_huggingface_dataset_failure(filepath): - def _mock(*args, **kwargs): - raise Exception("Download failed") - - return _mock - - -class TestCountyFIPSDatasetFile: - """ - Test that the county FIPS dataset file exists and downloads properly. - """ - - HUGGINGFACE_REPO = "policyengine/policyengine-us-data" - COUNTY_FIPS_DATASET_FILENAME = "county_fips_2020.csv.gz" - - def test_when_downloading_county_fips__download_is_successful(self, tmp_fips_dir): - download_huggingface_dataset( - repo=self.HUGGINGFACE_REPO, - repo_filename=self.COUNTY_FIPS_DATASET_FILENAME, - version=None, - local_dir=tmp_fips_dir, - ) - - TMP_FILE = tmp_fips_dir / self.COUNTY_FIPS_DATASET_FILENAME - assert TMP_FILE.is_file() - - def test_when_downloading_and_parsing_county_fips__result_is_correct( - self, tmp_fips_dir - ): - download_huggingface_dataset( - repo=self.HUGGINGFACE_REPO, - repo_filename=self.COUNTY_FIPS_DATASET_FILENAME, - version=None, - local_dir=tmp_fips_dir, - ) - - TMP_FILE = tmp_fips_dir / self.COUNTY_FIPS_DATASET_FILENAME - - df = pd.read_csv( - TMP_FILE, - compression="gzip", - dtype={"county_fips": str}, - encoding="utf-8", - nrows=5, # Just read a few rows - ) - - assert "county_fips" in df.columns - assert len(df) > 0 - - # Check FIPS codes are properly preserved as strings - assert all(isinstance(fips, str) for fips in df["county_fips"]) - - class TestLoadCountyFIPSDataset: """ Test that the load_county_fips_dataset function works correctly. """ - def test_when_func_is_run__correctly__returns_dataframe( + def test_when_local_data_file_exists__returns_local_dataframe( self, mock_dataset_file, monkeypatch ): """ - Test that the load_county_fips_dataset function returns a DataFrame with the correct columns. + Test that the load_county_fips_dataset function reads a local data file when present. """ - # Apply the mock monkeypatch.setattr( - "policyengine_us.tools.geography.county_helpers.download_huggingface_dataset", - mock_download_huggingface_dataset_success(mock_dataset_file), + county_helpers, + "DATA_FOLDER", + mock_dataset_file.parent, ) result = load_county_fips_dataset() - # Verify the result is a pandas DataFrame with expected structure assert isinstance(result, pd.DataFrame) assert len(result) == 3 assert ( "01001" in result.values ) # Check that FIPS codes are preserved as strings - def test_when_func_is_run__download_fails__raises_exception( - self, mock_dataset_file, monkeypatch + def test_when_local_data_file_is_missing__returns_packaged_dataframe( + self, tmp_fips_dir, monkeypatch ): """ - Test that the load_county_fips_dataset function raises an exception when download fails. + Test that the packaged dataset is used when no local data file exists. """ - # Apply the mock monkeypatch.setattr( - "policyengine_us.tools.geography.county_helpers.download_huggingface_dataset", - mock_download_huggingface_dataset_failure(mock_dataset_file), + county_helpers, + "DATA_FOLDER", + tmp_fips_dir, ) - with pytest.raises(Exception) as excinfo: - load_county_fips_dataset() + result = load_county_fips_dataset() - assert "Error downloading" in str(excinfo.value) + assert isinstance(result, pd.DataFrame) + assert {"county_fips", "county_name", "state"}.issubset(result.columns) + assert len(result) > 3_000 + assert "01001" in result["county_fips"].values + assert "06037" in result["county_fips"].values + assert all(isinstance(fips, str) for fips in result["county_fips"]) diff --git a/policyengine_us/tools/geography/county_helpers.py b/policyengine_us/tools/geography/county_helpers.py index 543709a0581..9efa740b4e5 100644 --- a/policyengine_us/tools/geography/county_helpers.py +++ b/policyengine_us/tools/geography/county_helpers.py @@ -1,46 +1,44 @@ -import pandas as pd +from importlib import resources +from pathlib import Path + import numpy as np +import pandas as pd + from policyengine_us.variables.household.demographic.geographic.county.county_enum import ( County, ) -from pathlib import Path -from policyengine_core.tools.hugging_face import download_huggingface_dataset -def load_county_fips_dataset() -> pd.DataFrame: - """ - Download the county FIPS dataset from Hugging Face and load it into a pandas DataFrame. - If the dataset already exists in the 'data' folder and is the most recent version, this - function will just load that into a pandas DataFrame. - """ +DATA_FOLDER = Path("data") +COUNTY_FIPS_DATASET_FILENAME = "county_fips_2020.csv.gz" +COUNTY_FIPS_PACKAGE = "policyengine_us.data" - DATA_FOLDER = Path("data") - HUGGINGFACE_REPO = "policyengine/policyengine-us-data" - COUNTY_FIPS_DATASET_FILENAME = "county_fips_2020.csv.gz" - try: - COUNTY_FIPS_RAW = download_huggingface_dataset( - repo=HUGGINGFACE_REPO, - repo_filename=COUNTY_FIPS_DATASET_FILENAME, - version=None, - local_dir=DATA_FOLDER, - ) +def _read_county_fips_dataset(dataset_file) -> pd.DataFrame: + return pd.read_csv( + dataset_file, + compression="gzip", + dtype={"county_fips": str}, + encoding="utf-8", + ) - # Read raw data into pandas dataframe; county FIPS MUST be defined as string, - # else pandas reads as int and drops leading zeros - COUNTY_FIPS_DATASET = pd.read_csv( - COUNTY_FIPS_RAW, - compression="gzip", - dtype={"county_fips": str}, - encoding="utf-8", - ) - return COUNTY_FIPS_DATASET +def load_county_fips_dataset() -> pd.DataFrame: + """ + Load the county FIPS dataset into a pandas DataFrame. + If the dataset exists in the 'data' folder, load that local copy. Otherwise, + use the packaged fallback so runtime county lookup does not require network access. + """ - except Exception as e: - raise Exception( - f"Error downloading {COUNTY_FIPS_DATASET_FILENAME} from {HUGGINGFACE_REPO}: {e}" - ) + local_dataset = DATA_FOLDER / COUNTY_FIPS_DATASET_FILENAME + if local_dataset.is_file(): + return _read_county_fips_dataset(local_dataset) + + package_dataset = resources.files(COUNTY_FIPS_PACKAGE).joinpath( + COUNTY_FIPS_DATASET_FILENAME + ) + with package_dataset.open("rb") as dataset_file: + return _read_county_fips_dataset(dataset_file) def map_county_string_to_enum( From 9f283d95c5d0b7aab588c7fa59ca9b5fbe712179 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:26:44 -0400 Subject: [PATCH 2/2] Fix changelog fragment path --- changelog.d/{fixed/8650.md => 8650.fixed.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{fixed/8650.md => 8650.fixed.md} (100%) diff --git a/changelog.d/fixed/8650.md b/changelog.d/8650.fixed.md similarity index 100% rename from changelog.d/fixed/8650.md rename to changelog.d/8650.fixed.md