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.
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/catalog-drift.yml +1 -1
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/pre-release.yml +1 -1
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/publish.yml +4 -4
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/smoke.yml +1 -1
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.github/workflows/test.yml +8 -5
- {eolas_data-1.3.14 → eolas_data-1.3.17}/PKG-INFO +8 -2
- {eolas_data-1.3.14 → eolas_data-1.3.17}/README.md +7 -1
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/__init__.py +1 -1
- {eolas_data-1.3.14 → eolas_data-1.3.17}/pyproject.toml +6 -1
- eolas_data-1.3.17/tests/fixtures/source_helpers.json +37 -0
- eolas_data-1.3.17/tests/test_changes_http.py +51 -0
- eolas_data-1.3.17/tests/test_get_local.py +276 -0
- eolas_data-1.3.17/tests/test_source_helpers.py +36 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/.gitignore +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/_dataset_names.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/_regen_names.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/cdc.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/cli.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/client.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/console.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/dataset.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/exceptions.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/library.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/meta.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/rows.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/schedule.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/eolas_data/search.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/scripts/check_client_contract.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/scripts/preflight.sh +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_as_arrow.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_cdc_roundtrip.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_cli.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_client.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_keyring.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_library.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_meta.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_progress.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_rows.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_schedule.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_search.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_smoke_live.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_sync_bulk.py +0 -0
- {eolas_data-1.3.14 → eolas_data-1.3.17}/tests/test_sync_changes.py +0 -0
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
version-parity:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
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@
|
|
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@
|
|
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@
|
|
87
|
+
- uses: actions/checkout@v5
|
|
88
88
|
|
|
89
89
|
- uses: actions/setup-python@v5
|
|
90
90
|
with:
|
|
@@ -9,7 +9,7 @@ jobs:
|
|
|
9
9
|
contract:
|
|
10
10
|
runs-on: ubuntu-latest
|
|
11
11
|
steps:
|
|
12
|
-
- uses: actions/checkout@
|
|
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@
|
|
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@
|
|
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@
|
|
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@
|
|
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.
|
|
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
|
+
[](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml)
|
|
52
|
+
[](https://codecov.io/gh/phildonovan/eolas-data)
|
|
53
|
+
[](https://pypi.org/project/eolas-data/)
|
|
54
|
+
[](https://pypi.org/project/eolas-data/)
|
|
55
|
+
[](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
|
+
[](https://github.com/phildonovan/eolas-data/actions/workflows/test.yml)
|
|
4
|
+
[](https://codecov.io/gh/phildonovan/eolas-data)
|
|
5
|
+
[](https://pypi.org/project/eolas-data/)
|
|
6
|
+
[](https://pypi.org/project/eolas-data/)
|
|
7
|
+
[](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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eolas-data"
|
|
7
|
-
version = "1.3.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|