diff --git a/.github/workflows/site-snapshot.yml b/.github/workflows/site-snapshot.yml index 1cca556..76dd0e1 100644 --- a/.github/workflows/site-snapshot.yml +++ b/.github/workflows/site-snapshot.yml @@ -41,6 +41,7 @@ jobs: run: | uv run --extra dev --with pydantic --with-editable ../microplex pytest -q \ tests/test_package_imports.py \ + tests/targets/test_supabase.py \ tests/pipelines/test_check_site_snapshot.py \ tests/pipelines/test_imputation_ablation.py \ tests/pipelines/test_site_snapshot.py \ diff --git a/pyproject.toml b/pyproject.toml index 82e532d..99ea739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ requires-python = ">=3.13" dependencies = [ "microplex[calibrate]", "duckdb>=1.2", + "requests>=2.31", ] [project.optional-dependencies] diff --git a/src/microplex_us/supabase_targets.py b/src/microplex_us/supabase_targets.py new file mode 100644 index 0000000..3309e15 --- /dev/null +++ b/src/microplex_us/supabase_targets.py @@ -0,0 +1,289 @@ +"""US Supabase calibration target loader.""" + +from __future__ import annotations + +import os +from typing import Any + +import requests + + +class SupabaseTargetLoader: + """Load US calibration targets from the microplex Supabase schema.""" + + # Mapping from Supabase variable names to CPS column names. + CPS_COLUMN_MAP = { + "employment_income": "employment_income", + "self_employment_income": "self_employment_income", + "dividend_income": "dividend_income", + "interest_income": "interest_income", + "rental_income": "rental_income", + "social_security": "social_security", + "unemployment_compensation": "unemployment_compensation", + "taxable_pension_income": "taxable_pension_income", + "tax_exempt_pension_income": "tax_exempt_pension_income", + "long_term_capital_gains": "long_term_capital_gains", + "short_term_capital_gains": "short_term_capital_gains", + "partnership_s_corp_income": "partnership_s_corp_income", + "farm_income": "farm_income", + "alimony_income": "alimony_income", + "snap_spending": "snap", + "ssi_spending": "ssi", + "eitc_spending": "eitc", + "social_security_spending": "social_security", + "unemployment_spending": "unemployment_compensation", + "medicaid_enrollment": "medicaid", + "aca_enrollment": "aca", + "snap_households": "snap", + "health_insurance_premiums": "health_insurance_premiums", + "other_medical_expenses": "medical_expenses", + } + + STATE_FIPS = { + "01": "al", + "02": "ak", + "04": "az", + "05": "ar", + "06": "ca", + "08": "co", + "09": "ct", + "10": "de", + "11": "dc", + "12": "fl", + "13": "ga", + "15": "hi", + "16": "id", + "17": "il", + "18": "in", + "19": "ia", + "20": "ks", + "21": "ky", + "22": "la", + "23": "me", + "24": "md", + "25": "ma", + "26": "mi", + "27": "mn", + "28": "ms", + "29": "mo", + "30": "mt", + "31": "ne", + "32": "nv", + "33": "nh", + "34": "nj", + "35": "nm", + "36": "ny", + "37": "nc", + "38": "nd", + "39": "oh", + "40": "ok", + "41": "or", + "42": "pa", + "44": "ri", + "45": "sc", + "46": "sd", + "47": "tn", + "48": "tx", + "49": "ut", + "50": "vt", + "51": "va", + "53": "wa", + "54": "wv", + "55": "wi", + "56": "wy", + } + + def __init__( + self, + url: str | None = None, + key: str | None = None, + schema: str = "microplex", + ) -> None: + """Initialize the loader. + + Args: + url: Supabase URL. Defaults to SUPABASE_URL env var. + key: Supabase key. Defaults to COSILICO_SUPABASE_SERVICE_KEY env var. + schema: Schema to use. Defaults to 'microplex'. + """ + self.url = url or os.environ.get( + "SUPABASE_URL", + "https://nsupqhfchdtqclomlrgs.supabase.co", + ) + self.key = key or os.environ.get("COSILICO_SUPABASE_SERVICE_KEY") + if not self.key: + raise ValueError( + "Supabase service key must be provided via the key argument or " + "COSILICO_SUPABASE_SERVICE_KEY." + ) + self.base_url = f"{self.url}/rest/v1" + self.headers = { + "apikey": self.key, + "Authorization": f"Bearer {self.key}", + "Content-Type": "application/json", + "Accept-Profile": schema, + "Content-Profile": schema, + } + self._cache = {} + + def _get( + self, + endpoint: str, + params: dict[str, Any] | None = None, + paginate: bool = True, + ) -> list[dict[str, Any]]: + """Make a GET request to Supabase with optional pagination.""" + url = f"{self.base_url}/{endpoint}" + params = params or {} + + if not paginate: + response = requests.get( + url, + headers=self.headers, + params=params, + timeout=30, + ) + response.raise_for_status() + return response.json() + + all_results = [] + offset = 0 + limit = 1000 + + while True: + page_params = {**params, "limit": limit, "offset": offset} + response = requests.get( + url, + headers=self.headers, + params=page_params, + timeout=30, + ) + response.raise_for_status() + results = response.json() + + if not results: + break + + all_results.extend(results) + offset += limit + + if len(results) < limit: + break + + return all_results + + def load_all(self, period: int | None = None) -> list[dict[str, Any]]: + """Load all targets with source and stratum info.""" + params = { + "select": "id,variable,value,target_type,period,notes,source:sources(id,name,institution),stratum:strata(id,name,jurisdiction)", + } + if period: + params["period"] = f"eq.{period}" + + return self._get("targets", params) + + def load_by_institution( + self, + institution: str, + period: int | None = None, + ) -> list[dict[str, Any]]: + """Load targets from a specific source institution.""" + sources = self._get("sources", {"institution": f"eq.{institution}"}) + source_ids = [source["id"] for source in sources] + + if not source_ids: + return [] + + params = { + "select": "id,variable,value,target_type,period,notes,source:sources(id,name,institution),stratum:strata(id,name,jurisdiction)", + "source_id": f"in.({','.join(source_ids)})", + } + if period: + params["period"] = f"eq.{period}" + + return self._get("targets", params) + + def load_by_period(self, period: int) -> list[dict[str, Any]]: + """Load targets for a specific year.""" + return self.load_all(period=period) + + def get_cps_column_map(self) -> dict[str, str]: + """Get the mapping from Supabase variable names to CPS columns.""" + return self.CPS_COLUMN_MAP.copy() + + def _parse_jurisdiction(self, jurisdiction: str) -> str | None: + """Parse jurisdiction to get the state code when applicable.""" + if jurisdiction in {"us", "us-national"}: + return None + + if jurisdiction.startswith("us-") and len(jurisdiction) == 5: + state = jurisdiction[3:].lower() + if len(state) == 2: + return state + + if jurisdiction.startswith("us-") and len(jurisdiction) == 5: + fips = jurisdiction[3:] + return self.STATE_FIPS.get(fips) + + return None + + def build_calibration_constraints( + self, + period: int = 2024, + include_states: bool = False, + target_types: list[str] | None = None, + ) -> dict[str, float]: + """Build a CPS-column calibration constraint dict from Supabase targets.""" + targets = self.load_all(period=period) + constraints = {} + + for target in targets: + variable = target["variable"] + value = target["value"] + target_type = target.get("target_type", "amount") + stratum = target.get("stratum", {}) + jurisdiction = stratum.get("jurisdiction", "us") + + if target_types and target_type not in target_types: + continue + + cps_col = self.CPS_COLUMN_MAP.get(variable) + if not cps_col: + continue + + state = self._parse_jurisdiction(jurisdiction) + + if state and include_states: + constraints[f"{cps_col}_{state}"] = value + elif not state and cps_col not in constraints: + constraints[cps_col] = value + + return constraints + + def get_summary(self) -> dict[str, Any]: + """Get summary counts for available targets in Supabase.""" + targets = self.load_all() + + by_institution = {} + by_variable = {} + by_type = {} + + for target in targets: + institution = target.get("source", {}).get("institution", "Unknown") + by_institution[institution] = by_institution.get(institution, 0) + 1 + + variable = target["variable"] + by_variable[variable] = by_variable.get(variable, 0) + 1 + + target_type = target.get("target_type", "amount") + by_type[target_type] = by_type.get(target_type, 0) + 1 + + return { + "total": len(targets), + "by_institution": by_institution, + "by_variable": by_variable, + "by_type": by_type, + } + + +__all__ = ["SupabaseTargetLoader"] diff --git a/tests/targets/test_supabase.py b/tests/targets/test_supabase.py new file mode 100644 index 0000000..238e3a1 --- /dev/null +++ b/tests/targets/test_supabase.py @@ -0,0 +1,248 @@ +"""Tests for loading US calibration targets from Supabase.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + +import pytest + +from microplex_us.supabase_targets import SupabaseTargetLoader + +SUPABASE_URL = "https://test.supabase.co" +SUPABASE_KEY = "test-key" + + +@dataclass +class MockResponse: + payload: list[dict[str, Any]] + + def json(self) -> list[dict[str, Any]]: + return self.payload + + def raise_for_status(self) -> None: + return None + + +@pytest.fixture +def loader() -> SupabaseTargetLoader: + return SupabaseTargetLoader(SUPABASE_URL, SUPABASE_KEY) + + +@pytest.fixture +def request_queue(monkeypatch: pytest.MonkeyPatch): + calls = [] + responses: list[MockResponse] = [] + + def fake_get( + url: str, + *, + headers: dict[str, str], + params: dict[str, Any], + timeout: int, + ) -> MockResponse: + calls.append( + { + "url": url, + "headers": headers, + "params": params, + "timeout": timeout, + } + ) + return responses.pop(0) + + def queue(*payloads: list[dict[str, Any]]) -> list[dict[str, Any]]: + responses[:] = [MockResponse(payload) for payload in payloads] + return calls + + monkeypatch.setattr("microplex_us.supabase_targets.requests.get", fake_get) + return queue + + +def test_missing_service_key_raises(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("COSILICO_SUPABASE_SERVICE_KEY", raising=False) + + with pytest.raises(ValueError, match="COSILICO_SUPABASE_SERVICE_KEY"): + SupabaseTargetLoader(SUPABASE_URL) + + +def test_load_all_targets(loader: SupabaseTargetLoader, request_queue) -> None: + calls = request_queue( + [ + { + "id": "t1", + "variable": "employment_income", + "value": 9022400000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "IRS SOI", "institution": "IRS"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + { + "id": "t2", + "variable": "snap_spending", + "value": 103100000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "USDA SNAP", "institution": "USDA"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + ] + ) + + targets = loader.load_all() + + assert [target["variable"] for target in targets] == [ + "employment_income", + "snap_spending", + ] + assert calls[0]["url"] == f"{SUPABASE_URL}/rest/v1/targets" + assert calls[0]["params"]["limit"] == 1000 + assert calls[0]["params"]["offset"] == 0 + + +def test_load_by_institution( + loader: SupabaseTargetLoader, + request_queue, +) -> None: + request_queue( + [{"id": "src-1", "institution": "IRS", "name": "IRS SOI"}], + [ + { + "id": "t1", + "variable": "employment_income", + "value": 9022400000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "IRS SOI", "institution": "IRS"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + } + ], + ) + + targets = loader.load_by_institution("IRS") + + assert len(targets) == 1 + assert targets[0]["source"]["institution"] == "IRS" + + +def test_load_by_period(loader: SupabaseTargetLoader, request_queue) -> None: + calls = request_queue( + [ + { + "id": "t1", + "variable": "employment_income", + "value": 9022400000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "IRS SOI", "institution": "IRS"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + } + ] + ) + + targets = loader.load_by_period(2024) + + assert len(targets) == 1 + assert calls[0]["params"]["period"] == "eq.2024" + + +def test_cps_column_mapping(loader: SupabaseTargetLoader) -> None: + mapping = loader.get_cps_column_map() + + assert mapping["employment_income"] == "employment_income" + assert mapping["self_employment_income"] == "self_employment_income" + assert mapping["dividend_income"] == "dividend_income" + assert mapping["snap_spending"] == "snap" + assert mapping["ssi_spending"] == "ssi" + assert mapping["eitc_spending"] == "eitc" + + +def test_build_continuous_targets( + loader: SupabaseTargetLoader, + request_queue, +) -> None: + request_queue( + [ + { + "id": "t1", + "variable": "employment_income", + "value": 9022400000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "IRS SOI", "institution": "IRS"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + { + "id": "t2", + "variable": "snap_spending", + "value": 103100000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "USDA SNAP", "institution": "USDA"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + ] + ) + + constraints = loader.build_calibration_constraints() + + assert constraints["employment_income"] == 9022400000000 + assert constraints["snap"] == 103100000000 + + +def test_build_state_targets( + loader: SupabaseTargetLoader, + request_queue, +) -> None: + request_queue( + [ + { + "id": "t1", + "variable": "medicaid_enrollment", + "value": 14000000, + "target_type": "count", + "period": 2024, + "source": {"name": "CMS Medicaid", "institution": "HHS"}, + "stratum": {"name": "California", "jurisdiction": "us-ca"}, + } + ] + ) + + constraints = loader.build_calibration_constraints(include_states=True) + + assert constraints["medicaid_ca"] == 14000000 + + +def test_get_summary(loader: SupabaseTargetLoader, request_queue) -> None: + request_queue( + [ + { + "id": "t1", + "variable": "employment_income", + "value": 9022400000000, + "target_type": "amount", + "period": 2024, + "source": {"name": "IRS SOI", "institution": "IRS"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + { + "id": "t2", + "variable": "person_count", + "value": 330000000, + "target_type": "count", + "period": 2024, + "source": {"name": "Census", "institution": "Census"}, + "stratum": {"name": "National", "jurisdiction": "us"}, + }, + ] + ) + + summary = loader.get_summary() + + assert summary == { + "total": 2, + "by_institution": {"IRS": 1, "Census": 1}, + "by_variable": {"employment_income": 1, "person_count": 1}, + "by_type": {"amount": 1, "count": 1}, + } diff --git a/uv.lock b/uv.lock index 9bf745a..bbf679b 100644 --- a/uv.lock +++ b/uv.lock @@ -305,6 +305,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, + { url = "https://files.pythonhosted.org/packages/94/2b/4d012a69759ac9d77210b8bfb128bc621125f5b20fc398bce3940d036b1c/greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd", size = 628268, upload-time = "2026-02-20T21:02:48.024Z" }, { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, @@ -313,6 +314,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ac/85804f74f1ccea31ba518dcc8ee6f14c79f73fe36fa1beba38930806df09/greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9", size = 675371, upload-time = "2026-02-20T21:02:49.664Z" }, { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, @@ -321,6 +323,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/d1/67/8197b7e7e602150938049d8e7f30de1660cfb87e4c8ee349b42b67bdb2e1/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf", size = 666581, upload-time = "2026-02-20T21:02:51.526Z" }, { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, @@ -726,7 +729,7 @@ requires-dist = [ { name = "cvxpy", marker = "extra == 'cvxpy'", specifier = ">=1.3" }, { name = "httpx", specifier = ">=0.25" }, { name = "huggingface-hub", specifier = ">=0.20" }, - { name = "jupyter-book", marker = "extra == 'docs'", specifier = ">=0.15" }, + { name = "jupyter-book", marker = "extra == 'docs'", specifier = ">=0.15,<2" }, { name = "l0-python", marker = "extra == 'l0'", specifier = ">=0.4" }, { name = "matplotlib", marker = "extra == 'benchmark'", specifier = ">=3.7" }, { name = "microcalibrate", marker = "python_full_version >= '3.13' and extra == 'calibrate'", specifier = ">=0.22" }, @@ -763,6 +766,7 @@ source = { editable = "." } dependencies = [ { name = "duckdb" }, { name = "microplex", extra = ["calibrate"] }, + { name = "requests" }, ] [package.optional-dependencies] @@ -782,6 +786,7 @@ requires-dist = [ { name = "microplex", extras = ["calibrate"], editable = "../microplex" }, { name = "policyengine-us", marker = "python_full_version >= '3.11' and python_full_version < '3.15' and extra == 'policyengine'", specifier = "==1.587.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0" }, + { name = "requests", specifier = ">=2.31" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1" }, ] provides-extras = ["dev", "policyengine"]