eolas-data 1.3.14__tar.gz → 1.3.17__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/catalog-drift.yml +1 -1
  2. {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/pre-release.yml +1 -1
  3. {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/publish.yml +4 -4
  4. {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/smoke.yml +1 -1
  5. {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/test.yml +8 -5
  6. {eolas_data-1.3.14 → eolas_data-1.3.17}/PKG-INFO +8 -2
  7. {eolas_data-1.3.14 → eolas_data-1.3.17}/README.md +7 -1
  8. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/__init__.py +1 -1
  9. {eolas_data-1.3.14 → eolas_data-1.3.17}/pyproject.toml +6 -1
  10. eolas_data-1.3.17/tests/fixtures/source_helpers.json +37 -0
  11. eolas_data-1.3.17/tests/test_changes_http.py +51 -0
  12. eolas_data-1.3.17/tests/test_get_local.py +276 -0
  13. eolas_data-1.3.17/tests/test_source_helpers.py +36 -0
  14. {eolas_data-1.3.14 → eolas_data-1.3.17}/.gitignore +0 -0
  15. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/_dataset_names.py +0 -0
  16. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/_regen_names.py +0 -0
  17. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/cdc.py +0 -0
  18. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/cli.py +0 -0
  19. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/client.py +0 -0
  20. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/console.py +0 -0
  21. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/dataset.py +0 -0
  22. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/exceptions.py +0 -0
  23. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/library.py +0 -0
  24. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/meta.py +0 -0
  25. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/rows.py +0 -0
  26. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/schedule.py +0 -0
  27. {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/search.py +0 -0
  28. {eolas_data-1.3.14 → eolas_data-1.3.17}/scripts/check_client_contract.py +0 -0
  29. {eolas_data-1.3.14 → eolas_data-1.3.17}/scripts/preflight.sh +0 -0
  30. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_as_arrow.py +0 -0
  31. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_cdc_roundtrip.py +0 -0
  32. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_cli.py +0 -0
  33. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_client.py +0 -0
  34. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_keyring.py +0 -0
  35. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_library.py +0 -0
  36. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_meta.py +0 -0
  37. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_progress.py +0 -0
  38. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_rows.py +0 -0
  39. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_schedule.py +0 -0
  40. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_search.py +0 -0
  41. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_smoke_live.py +0 -0
  42. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_sync_bulk.py +0 -0
  43. {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_sync_changes.py +0 -0
@@ -17,7 +17,7 @@ jobs:
17
17
  check:
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
- - uses: actions/checkout@v4
20
+ - uses: actions/checkout@v5
21
21
 
22
22
  - uses: actions/setup-python@v5
23
23
  with:
@@ -9,7 +9,7 @@ jobs:
9
9
  env:
10
10
  PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
  - uses: actions/setup-python@v5
14
14
  with:
15
15
  python-version: "3.12"
@@ -9,7 +9,7 @@ jobs:
9
9
  version-parity:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
  - name: Compare tag to eolas-r DESCRIPTION
14
14
  env:
15
15
  TAG: ${{ github.ref_name }}
@@ -34,7 +34,7 @@ jobs:
34
34
  env:
35
35
  PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
36
36
  steps:
37
- - uses: actions/checkout@v4
37
+ - uses: actions/checkout@v5
38
38
 
39
39
  - uses: actions/setup-python@v5
40
40
  with:
@@ -59,7 +59,7 @@ jobs:
59
59
  runs-on: ubuntu-latest
60
60
  needs: test
61
61
  steps:
62
- - uses: actions/checkout@v4
62
+ - uses: actions/checkout@v5
63
63
  - uses: actions/setup-python@v5
64
64
  with:
65
65
  python-version: "3.12"
@@ -84,7 +84,7 @@ jobs:
84
84
  id-token: write
85
85
 
86
86
  steps:
87
- - uses: actions/checkout@v4
87
+ - uses: actions/checkout@v5
88
88
 
89
89
  - uses: actions/setup-python@v5
90
90
  with:
@@ -9,7 +9,7 @@ jobs:
9
9
  smoke:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
 
14
14
  - uses: actions/setup-python@v5
15
15
  with:
@@ -9,7 +9,7 @@ jobs:
9
9
  contract:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
- - uses: actions/checkout@v4
12
+ - uses: actions/checkout@v5
13
13
  - uses: actions/setup-python@v5
14
14
  with:
15
15
  python-version: "3.12"
@@ -26,7 +26,7 @@ jobs:
26
26
  env:
27
27
  PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
28
28
  steps:
29
- - uses: actions/checkout@v4
29
+ - uses: actions/checkout@v5
30
30
 
31
31
  - uses: actions/setup-python@v5
32
32
  with:
@@ -44,7 +44,7 @@ jobs:
44
44
  env:
45
45
  PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
46
46
  steps:
47
- - uses: actions/checkout@v4
47
+ - uses: actions/checkout@v5
48
48
 
49
49
  - uses: actions/setup-python@v5
50
50
  with:
@@ -65,17 +65,20 @@ jobs:
65
65
  coverage:
66
66
  runs-on: ubuntu-latest
67
67
  needs: contract
68
+ permissions:
69
+ id-token: write
68
70
  env:
69
71
  PYTHON_KEYRING_BACKEND: keyring.backends.null.Keyring
70
72
  steps:
71
- - uses: actions/checkout@v4
73
+ - uses: actions/checkout@v5
72
74
  - uses: actions/setup-python@v5
73
75
  with:
74
76
  python-version: "3.12"
75
77
  - run: pip install -e ".[dev,geo,secure]" pytest-cov
76
78
  - run: pytest -q -m "not integration" --cov=eolas_data --cov-report=xml
77
79
  - name: Upload coverage
78
- uses: codecov/codecov-action@v4
80
+ uses: codecov/codecov-action@v5
79
81
  with:
80
82
  files: coverage.xml
83
+ token: ${{ secrets.CODECOV_TOKEN }}
81
84
  fail_ci_if_error: false
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eolas-data
3
- Version: 1.3.14
3
+ Version: 1.3.17
4
4
  Summary: Python client for the eolas.fyi statistical data API (NZ, Australia, OECD)
5
5
  Project-URL: Homepage, https://eolas.fyi
6
6
  Project-URL: Documentation, https://docs.eolas.fyi/
@@ -48,6 +48,12 @@ Description-Content-Type: text/markdown
48
48
 
49
49
  # eolas-data
50
50
 
51
+ [![Tests](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml/badge.svg)](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml)
52
+ [![codecov](https://codecov.io/gh/phildonovan/eolas-data/graph/badge.svg)](https://codecov.io/gh/phildonovan/eolas-data)
53
+ [![PyPI](https://img.shields.io/pypi/v/eolas-data)](https://pypi.org/project/eolas-data/)
54
+ [![Python](https://img.shields.io/pypi/pyversions/eolas-data)](https://pypi.org/project/eolas-data/)
55
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
56
+
51
57
  Python client for the [eolas.fyi](https://eolas.fyi) statistical data API — 1,400+ official New Zealand statistical & geospatial datasets, plus OECD data for international comparisons, served as tidy `pandas` DataFrames (or `polars` / `geopandas` if you prefer).
52
58
 
53
59
  _Coverage is New Zealand + OECD today. Australian sources are on the roadmap — not yet available; OECD data already includes Australia (and other OECD members) for cross-country comparisons._
@@ -316,7 +322,7 @@ pytest -q -m "not integration"
316
322
  EOLAS_API_KEY=vs_... pytest -q -m integration tests/test_smoke_live.py
317
323
  ```
318
324
 
319
- CI runs the unit suite on Python 3.10, 3.12, and 3.13 on every push/PR. A weekly workflow optionally runs live smoke tests when `EOLAS_API_KEY` is configured as a repository secret.
325
+ CI runs the unit suite on Python 3.10, 3.12, and 3.13 on every push/PR, with [coverage](https://codecov.io/gh/phildonovan/eolas-data) uploaded to Codecov. A weekly workflow optionally runs live smoke tests when `EOLAS_API_KEY` is configured as a repository secret.
320
326
 
321
327
  ## Releasing
322
328
 
@@ -1,5 +1,11 @@
1
1
  # eolas-data
2
2
 
3
+ [![Tests](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml/badge.svg)](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml)
4
+ [![codecov](https://codecov.io/gh/phildonovan/eolas-data/graph/badge.svg)](https://codecov.io/gh/phildonovan/eolas-data)
5
+ [![PyPI](https://img.shields.io/pypi/v/eolas-data)](https://pypi.org/project/eolas-data/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/eolas-data)](https://pypi.org/project/eolas-data/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
3
9
  Python client for the [eolas.fyi](https://eolas.fyi) statistical data API — 1,400+ official New Zealand statistical & geospatial datasets, plus OECD data for international comparisons, served as tidy `pandas` DataFrames (or `polars` / `geopandas` if you prefer).
4
10
 
5
11
  _Coverage is New Zealand + OECD today. Australian sources are on the roadmap — not yet available; OECD data already includes Australia (and other OECD members) for cross-country comparisons._
@@ -268,7 +274,7 @@ pytest -q -m "not integration"
268
274
  EOLAS_API_KEY=vs_... pytest -q -m integration tests/test_smoke_live.py
269
275
  ```
270
276
 
271
- CI runs the unit suite on Python 3.10, 3.12, and 3.13 on every push/PR. A weekly workflow optionally runs live smoke tests when `EOLAS_API_KEY` is configured as a repository secret.
277
+ CI runs the unit suite on Python 3.10, 3.12, and 3.13 on every push/PR, with [coverage](https://codecov.io/gh/phildonovan/eolas-data) uploaded to Codecov. A weekly workflow optionally runs live smoke tests when `EOLAS_API_KEY` is configured as a repository secret.
272
278
 
273
279
  ## Releasing
274
280
 
@@ -15,7 +15,7 @@ from .exceptions import (
15
15
  WatermarkExpired,
16
16
  )
17
17
 
18
- __version__ = "1.3.13"
18
+ __version__ = "1.3.17"
19
19
 
20
20
  __all__ = [
21
21
  "Client",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eolas-data"
7
- version = "1.3.14"
7
+ version = "1.3.17"
8
8
  description = "Python client for the eolas.fyi statistical data API (NZ, Australia, OECD)"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -58,3 +58,8 @@ packages = ["eolas_data"]
58
58
  markers = [
59
59
  "integration: marks tests as requiring live API access (deselected by default)",
60
60
  ]
61
+
62
+ [tool.coverage.run]
63
+ omit = [
64
+ "eolas_data/_regen_names.py",
65
+ ]
@@ -0,0 +1,37 @@
1
+ {
2
+ "acc": "ACC",
3
+ "akl_council": "Auckland Council",
4
+ "akl_transport": "Auckland Transport",
5
+ "bay_of_plenty": "Bay of Plenty Councils",
6
+ "charities": "Charities Services",
7
+ "colab_waikato": "Co-Lab Waikato",
8
+ "doc": "DOC",
9
+ "ecan_canterbury": "ECan / Canterbury",
10
+ "edcounts": "Education Counts",
11
+ "eeca": "EECA",
12
+ "geonet": "GeoNet",
13
+ "hawkes_bay": "Hawke's Bay Councils",
14
+ "immigration": "Immigration NZ",
15
+ "linz": "LINZ",
16
+ "lris": "Manaaki Whenua / LRIS",
17
+ "manawatu_whanganui": "Manawatu-Whanganui Councils",
18
+ "mbie": "MBIE",
19
+ "msd": "MSD",
20
+ "napier_whanganui": "Napier + Whanganui",
21
+ "northland": "Northland Councils",
22
+ "nzta": "Waka Kotahi",
23
+ "oecd": "OECD",
24
+ "otago": "Otago Councils",
25
+ "pharmac": "PHARMAC",
26
+ "police": "NZ Police / MoJ",
27
+ "rbnz": "RBNZ",
28
+ "southland": "Southland Councils",
29
+ "statsnz": "Stats NZ",
30
+ "statsnz_geo": "Stats NZ",
31
+ "taranaki": "Taranaki Councils",
32
+ "top_of_south": "Gisborne / Top of South Councils",
33
+ "treasury": "NZ Treasury",
34
+ "wellington": "Wellington Region Councils",
35
+ "west_coast": "West Coast (Te Tai o Poutini)",
36
+ "worksafe": "WorkSafe NZ"
37
+ }
@@ -0,0 +1,51 @@
1
+ """HTTP error-path tests for Client._raw_changes_get (changelog endpoint)."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+ import responses as resp_lib
6
+
7
+ from eolas_data import Client
8
+ from eolas_data.exceptions import (
9
+ AuthenticationError,
10
+ ChangesLicenceRestricted,
11
+ ChangesUpgradeRequired,
12
+ WatermarkExpired,
13
+ )
14
+
15
+ BASE = "https://api.eolas.fyi"
16
+
17
+
18
+ @pytest.fixture()
19
+ def client():
20
+ return Client("eolas_testkey123", base_url=BASE)
21
+
22
+
23
+ @resp_lib.activate
24
+ @pytest.mark.parametrize(
25
+ "status,body,exc_cls,match",
26
+ [
27
+ (402, {"detail": "Pro required"}, ChangesUpgradeRequired, "Pro"),
28
+ (
29
+ 403,
30
+ {"detail": "licence: OECD prohibits export"},
31
+ ChangesLicenceRestricted,
32
+ "licence",
33
+ ),
34
+ (403, {"detail": "API key is inactive"}, AuthenticationError, "inactive"),
35
+ (
36
+ 410,
37
+ {"error": "watermark_expired", "min_available_seq": 42},
38
+ WatermarkExpired,
39
+ "watermark",
40
+ ),
41
+ ],
42
+ )
43
+ def test_raw_changes_get_typed_errors(client, status, body, exc_cls, match):
44
+ resp_lib.add(
45
+ resp_lib.GET,
46
+ f"{BASE}/v1/datasets/nz_parcels/changes",
47
+ json=body,
48
+ status=status,
49
+ )
50
+ with pytest.raises(exc_cls, match=match):
51
+ client._raw_changes_get("/v1/datasets/nz_parcels/changes", params={"since_seq": 0})
@@ -0,0 +1,276 @@
1
+ """Tests for Client.get_local() — mirror of eolas-r/tests/testthat/test-get-local.R."""
2
+ from __future__ import annotations
3
+
4
+ import gzip
5
+ import pathlib
6
+ from unittest.mock import patch
7
+
8
+ import pandas as pd
9
+ import pytest
10
+
11
+ from eolas_data import Client, SyncResult
12
+ from eolas_data.exceptions import (
13
+ BulkLicenceRestricted,
14
+ BulkNotYetAvailable,
15
+ BulkUpgradeRequired,
16
+ )
17
+
18
+ BASE = "https://api.eolas.fyi"
19
+ SNAPSHOT_ID = "snap_abc123"
20
+
21
+ BULK_DATASET_META = {
22
+ "name": "nz_cpi",
23
+ "title": "NZ CPI",
24
+ "source": "Stats NZ",
25
+ "namespace": "statsnz",
26
+ "table": "nz_cpi",
27
+ }
28
+
29
+ GEO_DATASET_META = {
30
+ "name": "nz_parcels",
31
+ "title": "NZ Parcels",
32
+ "source": "LINZ",
33
+ "namespace": "linz",
34
+ "table": "nz_parcels",
35
+ "geometry_type": "MultiPolygon",
36
+ "bulk_export_class": "geoparquet",
37
+ "row_count_at_last_refresh": 3_000_000,
38
+ }
39
+
40
+
41
+ @pytest.fixture()
42
+ def client():
43
+ return Client("eolas_testkey123", base_url=BASE)
44
+
45
+
46
+ def _sync_downloaded(name, *, path, format, **kwargs) -> SyncResult:
47
+ return SyncResult(
48
+ status="downloaded",
49
+ previous_snapshot_id=None,
50
+ current_snapshot_id=SNAPSHOT_ID,
51
+ path=path,
52
+ bytes_downloaded=1024,
53
+ )
54
+
55
+
56
+ def _sync_unchanged(name, *, path, format, **kwargs) -> SyncResult:
57
+ return SyncResult(
58
+ status="unchanged",
59
+ previous_snapshot_id=SNAPSHOT_ID,
60
+ current_snapshot_id=SNAPSHOT_ID,
61
+ path=path,
62
+ bytes_downloaded=0,
63
+ )
64
+
65
+
66
+ def _write_csv_gz(path: pathlib.Path) -> None:
67
+ path.parent.mkdir(parents=True, exist_ok=True)
68
+ with gzip.open(path, "wt", encoding="utf-8") as fh:
69
+ fh.write("date,value\n2023-01-01,1100.5\n")
70
+
71
+
72
+ # ---------------------------------------------------------------------------
73
+ # First call / cached read
74
+ # ---------------------------------------------------------------------------
75
+
76
+ def test_get_local_returns_dataset_csv_gz(client, tmp_path):
77
+ """First call reads a csv_gz bulk file into a Dataset."""
78
+
79
+ def fake_sync(name, *, path, format, **kwargs):
80
+ _write_csv_gz(path)
81
+ return _sync_downloaded(name, path=path, format=format)
82
+
83
+ with (
84
+ patch.object(client, "_info_cached", return_value=BULK_DATASET_META),
85
+ patch.object(client, "sync_bulk", side_effect=fake_sync),
86
+ ):
87
+ result = client.get_local(
88
+ "nz_cpi", format="csv_gz", cache_dir=tmp_path,
89
+ )
90
+
91
+ assert isinstance(result, pd.DataFrame)
92
+ assert list(result.columns) == ["date", "value"]
93
+ assert len(result) == 1
94
+ assert result["value"].iloc[0] == pytest.approx(1100.5)
95
+
96
+
97
+ def test_get_local_reads_cached_file_on_subsequent_call(client, tmp_path):
98
+ """Unchanged sync_bulk → read existing csv.gz without re-downloading."""
99
+ csv_path = tmp_path / "nz_cpi.csv.gz"
100
+ with gzip.open(csv_path, "wt", encoding="utf-8") as fh:
101
+ fh.write("date,value\n2023-06-01,1105.0\n")
102
+
103
+ with (
104
+ patch.object(client, "_info_cached", return_value=BULK_DATASET_META),
105
+ patch.object(client, "sync_bulk", side_effect=_sync_unchanged),
106
+ ):
107
+ result = client.get_local("nz_cpi", format="csv_gz", cache_dir=tmp_path)
108
+
109
+ assert isinstance(result, pd.DataFrame)
110
+ assert len(result) == 1
111
+ assert result["value"].iloc[0] == pytest.approx(1105.0)
112
+
113
+
114
+ def test_get_local_expands_tilde_in_cache_dir(client, tmp_path, monkeypatch):
115
+ """cache_dir='~/…' is expanded to an absolute path before sync_bulk."""
116
+ home = tmp_path / "fakehome"
117
+ home.mkdir()
118
+ monkeypatch.setenv("HOME", str(home))
119
+
120
+ seen_path = {"path": None}
121
+
122
+ def fake_sync(name, *, path, format, **kwargs):
123
+ seen_path["path"] = path
124
+ _write_csv_gz(path)
125
+ return _sync_downloaded(name, path=path, format=format)
126
+
127
+ with patch.object(client, "sync_bulk", side_effect=fake_sync):
128
+ client.get_local("nz_cpi", format="csv_gz", cache_dir="~/.cache/eolas", meta=False)
129
+
130
+ expected = (home / ".cache" / "eolas" / "nz_cpi.csv.gz").resolve()
131
+ assert seen_path["path"] == expected
132
+ assert "~" not in str(seen_path["path"])
133
+
134
+
135
+ def test_get_local_explicit_cache_dir_overrides_library(client, tmp_path, monkeypatch):
136
+ """Explicit cache_dir wins over EOLAS_LIBRARY / config resolution."""
137
+ import eolas_data.library as lib
138
+
139
+ env_dir = tmp_path / "from_env"
140
+ monkeypatch.setenv("EOLAS_LIBRARY", str(env_dir))
141
+ explicit = tmp_path / "explicit_cache"
142
+
143
+ def fake_sync(name, *, path, format, **kwargs):
144
+ _write_csv_gz(path)
145
+ return _sync_downloaded(name, path=path, format=format)
146
+
147
+ with patch.object(client, "sync_bulk", side_effect=fake_sync):
148
+ client.get_local("nz_cpi", format="csv_gz", cache_dir=explicit, meta=False)
149
+
150
+ assert (explicit / "nz_cpi.csv.gz").exists()
151
+ assert not (env_dir / "nz_cpi.csv.gz").exists()
152
+ # Sanity: library resolver would have picked env_dir
153
+ assert lib.resolve_library_dir(interactive=False) == env_dir.resolve()
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Format auto-detection
158
+ # ---------------------------------------------------------------------------
159
+
160
+ @pytest.mark.parametrize(
161
+ "meta,expected_fmt",
162
+ [
163
+ (GEO_DATASET_META, "geoparquet"),
164
+ (BULK_DATASET_META, "parquet"),
165
+ (
166
+ {
167
+ "name": "rbnz_b2_wholesale_rates_monthly",
168
+ "namespace": "rbnz",
169
+ "table": "rbnz_b2_wholesale_rates_monthly",
170
+ "geometry_type": "none",
171
+ },
172
+ "parquet",
173
+ ),
174
+ ],
175
+ )
176
+ def test_get_local_auto_detects_format(client, tmp_path, meta, expected_fmt):
177
+ """Geo metadata → geoparquet; tabular / geometry_type='none' → parquet."""
178
+ seen = {"format": None}
179
+
180
+ def fake_sync(name, *, path, format, **kwargs):
181
+ seen["format"] = format
182
+ raise OSError("skip read — format detection only")
183
+
184
+ with (
185
+ patch.object(client, "_info_cached", return_value=meta),
186
+ patch.object(client, "sync_bulk", side_effect=fake_sync),
187
+ pytest.raises(OSError, match="skip read"),
188
+ ):
189
+ client.get_local(meta["name"], cache_dir=tmp_path, meta=True)
190
+
191
+ assert seen["format"] == expected_fmt
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Error propagation + validation
196
+ # ---------------------------------------------------------------------------
197
+
198
+ def test_get_local_as_arrow_and_as_geo_conflict(client, tmp_path):
199
+ with pytest.raises(ValueError, match="mutually exclusive"):
200
+ client.get_local("nz_cpi", cache_dir=tmp_path, as_arrow=True, as_geo=True)
201
+
202
+
203
+ def test_get_local_unknown_format_raises(client, tmp_path):
204
+ with pytest.raises(ValueError, match="Unknown format"):
205
+ client.get_local("nz_cpi", format="xml", cache_dir=tmp_path, meta=False)
206
+
207
+
208
+ @pytest.mark.parametrize(
209
+ "exc_cls,message",
210
+ [
211
+ (BulkUpgradeRequired, "Bulk upgrade required"),
212
+ (BulkLicenceRestricted, "Bulk licence restricted"),
213
+ (BulkNotYetAvailable, "Bulk not yet available"),
214
+ ],
215
+ )
216
+ def test_get_local_propagates_bulk_errors(client, tmp_path, exc_cls, message):
217
+ with (
218
+ patch.object(client, "_info_cached", return_value=BULK_DATASET_META),
219
+ patch.object(client, "sync_bulk", side_effect=exc_cls(message)),
220
+ pytest.raises(exc_cls, match=message.split()[1]), # partial match
221
+ ):
222
+ client.get_local("nz_cpi", cache_dir=tmp_path)
223
+
224
+
225
+ def test_get_local_parquet_histogram_fallback_to_csv_gz(client, tmp_path):
226
+ """OSError mentioning 'histogram' triggers csv_gz re-sync + read."""
227
+ parquet_path = tmp_path / "nz_cpi.parquet"
228
+ csv_path = tmp_path / "nz_cpi.csv.gz"
229
+ calls: list[tuple[str, str]] = []
230
+
231
+ def fake_sync(name, *, path, format, **kwargs):
232
+ calls.append((format, path.name))
233
+ if format == "csv_gz":
234
+ _write_csv_gz(path)
235
+ return _sync_downloaded(name, path=path, format=format)
236
+ path.touch()
237
+ return _sync_downloaded(name, path=path, format=format)
238
+
239
+ real_read_parquet = pd.read_parquet
240
+
241
+ def flaky_read_parquet(path, *args, **kwargs):
242
+ if path == parquet_path:
243
+ raise OSError("Repetition level histogram size mismatch")
244
+ return real_read_parquet(path, *args, **kwargs)
245
+
246
+ with (
247
+ patch.object(client, "_info_cached", return_value=BULK_DATASET_META),
248
+ patch.object(client, "sync_bulk", side_effect=fake_sync),
249
+ patch("eolas_data.client.pd.read_parquet", side_effect=flaky_read_parquet),
250
+ ):
251
+ result = client.get_local("nz_cpi", cache_dir=tmp_path, meta=True)
252
+
253
+ assert isinstance(result, pd.DataFrame)
254
+ assert calls[0] == ("parquet", "nz_cpi.parquet")
255
+ assert ("csv_gz", "nz_cpi.csv.gz") in calls
256
+ assert csv_path.exists()
257
+
258
+
259
+ def test_get_local_as_arrow_returns_table(client, tmp_path):
260
+ """as_arrow=True returns a pyarrow.Table (no pandas materialisation)."""
261
+ pytest.importorskip("pyarrow")
262
+ pytest.importorskip("pyarrow.parquet")
263
+ import pyarrow as pa
264
+ import pyarrow.parquet as pq
265
+
266
+ parquet_path = tmp_path / "nz_cpi.parquet"
267
+ table = pa.table({"date": ["2023-01-01"], "value": [1100.5]})
268
+ pq.write_table(table, parquet_path)
269
+
270
+ with patch.object(client, "sync_bulk", side_effect=_sync_unchanged):
271
+ result = client.get_local(
272
+ "nz_cpi", cache_dir=tmp_path, as_arrow=True, meta=False,
273
+ )
274
+
275
+ assert isinstance(result, pa.Table)
276
+ assert result.num_rows == 1
@@ -0,0 +1,36 @@
1
+ """Parametrised smoke tests for Client source-specific fetch helpers."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ import responses as resp_lib
8
+
9
+ from eolas_data import Client
10
+
11
+ BASE = "https://api.eolas.fyi"
12
+ RECORDS = [
13
+ {"date": "2023-01-01", "period": "2023Q1", "value": 100.0},
14
+ {"date": "2023-04-01", "period": "2023Q2", "value": 101.5},
15
+ ]
16
+
17
+ _SOURCE_HELPERS = json.loads(
18
+ (Path(__file__).parent / "fixtures" / "source_helpers.json").read_text(encoding="utf-8")
19
+ )
20
+
21
+
22
+ @pytest.fixture()
23
+ def client():
24
+ return Client("eolas_testkey123", base_url=BASE)
25
+
26
+
27
+ @pytest.mark.parametrize(
28
+ "method,expected_source",
29
+ sorted(_SOURCE_HELPERS.items()),
30
+ ids=sorted(_SOURCE_HELPERS.keys()),
31
+ )
32
+ @resp_lib.activate
33
+ def test_source_helper_sets_source(client, method, expected_source):
34
+ resp_lib.add(resp_lib.GET, f"{BASE}/v1/datasets/nz_cpi/data", json={"data": RECORDS})
35
+ df = getattr(client, method)("nz_cpi")
36
+ assert df.eolas_source == expected_source
File without changes