Skip to content

Commit 653267a

Browse files
authored
Allow offline imports with bundled data certification (#345)
* Allow offline imports with bundled data certification * Add offline import changelog
1 parent d206a61 commit 653267a

3 files changed

Lines changed: 86 additions & 11 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Country model imports now work without network access when the bundled release manifest already certifies the installed country package version. Hugging Face release-manifest transport failures fall back to bundled data certification only when the runtime model version and data build fingerprint gates still match.

src/policyengine/provenance/manifest.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,11 +236,17 @@ def get_data_release_manifest(country_id: str) -> DataReleaseManifest:
236236
if token:
237237
headers["Authorization"] = f"Bearer {token}"
238238

239-
response = requests.get(
240-
https_release_manifest_uri(country_manifest.data_package),
241-
headers=headers,
242-
timeout=HF_REQUEST_TIMEOUT_SECONDS,
243-
)
239+
try:
240+
response = requests.get(
241+
https_release_manifest_uri(country_manifest.data_package),
242+
headers=headers,
243+
timeout=HF_REQUEST_TIMEOUT_SECONDS,
244+
)
245+
except requests.RequestException as exc:
246+
raise DataReleaseManifestUnavailableError(
247+
"Could not fetch the data release manifest from Hugging Face."
248+
) from exc
249+
244250
if response.status_code in (401, 403):
245251
raise DataReleaseManifestUnavailableError(
246252
"Could not fetch the data release manifest from Hugging Face. "
@@ -250,7 +256,12 @@ def get_data_release_manifest(country_id: str) -> DataReleaseManifest:
250256
raise DataReleaseManifestUnavailableError(
251257
"No data release manifest was published for this data package."
252258
)
253-
response.raise_for_status()
259+
try:
260+
response.raise_for_status()
261+
except requests.RequestException as exc:
262+
raise DataReleaseManifestUnavailableError(
263+
"Could not fetch the data release manifest from Hugging Face."
264+
) from exc
254265
return DataReleaseManifest.model_validate_json(response.text)
255266

256267

tests/test_release_manifests.py

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"""Tests for bundled compatibility manifests and data release manifests."""
22

33
import json
4+
import os
45
import re
6+
import subprocess
7+
import sys
58
from pathlib import Path
69
from unittest.mock import MagicMock, patch
710

@@ -316,6 +319,22 @@ def test__given_private_manifest_unavailable__then_bundled_certification_is_used
316319

317320
assert certification == get_release_manifest("us").certification
318321

322+
def test__given_manifest_request_timeout__then_bundled_certification_is_used(
323+
self,
324+
):
325+
get_data_release_manifest.cache_clear()
326+
327+
with patch(
328+
"policyengine.provenance.manifest.requests.get",
329+
side_effect=Timeout("network timeout"),
330+
):
331+
certification = certify_data_release_compatibility(
332+
"us",
333+
runtime_model_version="1.687.0",
334+
)
335+
336+
assert certification == get_release_manifest("us").certification
337+
319338
def test__given_private_manifest_unavailable_and_fingerprint_mismatch__then_fails(
320339
self,
321340
):
@@ -348,24 +367,68 @@ def test__given_private_manifest_unavailable_and_fingerprint_mismatch__then_fail
348367
else:
349368
raise AssertionError("Expected fingerprint mismatch to fail")
350369

351-
def test__given_manifest_fetch_failure__then_certification_does_not_fallback(
370+
def test__given_manifest_fetch_failure_and_version_mismatch__then_fallback_fails(
352371
self,
353372
):
354373
get_data_release_manifest.cache_clear()
355374

356375
with patch(
357-
"policyengine.provenance.manifest.get_data_release_manifest",
376+
"policyengine.provenance.manifest.requests.get",
358377
side_effect=Timeout("network timeout"),
359378
):
360379
try:
361380
certify_data_release_compatibility(
362381
"us",
363382
runtime_model_version="1.602.0",
364383
)
365-
except Timeout as error:
366-
assert "network timeout" in str(error)
384+
except DataReleaseManifestUnavailableError as error:
385+
assert "Could not fetch" in str(error)
367386
else:
368-
raise AssertionError("Expected timeout to propagate")
387+
raise AssertionError("Expected offline mismatched version to fail")
388+
389+
def test__given_offline_hf__then_us_import_uses_bundled_certification(
390+
self,
391+
tmp_path,
392+
):
393+
sitecustomize = tmp_path / "sitecustomize.py"
394+
sitecustomize.write_text(
395+
"\n".join(
396+
[
397+
"import requests",
398+
"from requests import Timeout",
399+
"",
400+
"def offline_get(*args, **kwargs):",
401+
" raise Timeout('offline')",
402+
"",
403+
"requests.get = offline_get",
404+
]
405+
)
406+
)
407+
env = os.environ.copy()
408+
existing_pythonpath = env.get("PYTHONPATH")
409+
env["PYTHONPATH"] = (
410+
f"{tmp_path}{os.pathsep}{existing_pythonpath}"
411+
if existing_pythonpath
412+
else str(tmp_path)
413+
)
414+
415+
result = subprocess.run(
416+
[
417+
sys.executable,
418+
"-c",
419+
(
420+
"import policyengine.tax_benefit_models.us as us; "
421+
"print(us.model.data_certification.certified_by)"
422+
),
423+
],
424+
capture_output=True,
425+
text=True,
426+
check=False,
427+
env=env,
428+
)
429+
430+
assert result.returncode == 0, result.stderr
431+
assert "policyengine.py bundled manifest" in result.stdout
369432

370433
def test__given_mismatched_version_and_fingerprint__then_certification_fails(self):
371434
get_data_release_manifest.cache_clear()

0 commit comments

Comments
 (0)