eolas-data 1.3.22__tar.gz → 1.4.0__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.22 → eolas_data-1.4.0}/CHANGELOG.md +13 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/PKG-INFO +1 -1
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/__init__.py +1 -1
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/cli.py +16 -4
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/client.py +38 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/pyproject.toml +1 -1
- eolas_data-1.4.0/tests/test_preview_dimensions.py +82 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/catalog-drift.yml +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/pre-release.yml +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/publish.yml +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/smoke.yml +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/test.yml +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/.gitignore +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/LICENSE +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/README.md +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/_dataset_names.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/_regen_names.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/cdc.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/console.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/dataset.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/exceptions.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/library.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/meta.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/rows.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/schedule.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/search.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/scripts/check_client_contract.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/scripts/preflight.sh +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/fixtures/source_helpers.json +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_as_arrow.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_cdc_roundtrip.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_changes_http.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_cli.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_client.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_date_filter.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_get_local.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_keyring.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_library.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_meta.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_progress.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_resilience.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_rows.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_schedule.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_search.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_smoke_live.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_source_helpers.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_sync_bulk.py +0 -0
- {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_sync_changes.py +0 -0
|
@@ -3,6 +3,19 @@
|
|
|
3
3
|
All notable changes to `eolas-data` are recorded here. This project follows
|
|
4
4
|
[Semantic Versioning](https://semver.org/).
|
|
5
5
|
|
|
6
|
+
## 1.4.0
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`Client.preview(name)`** — up to 10 sample rows via the unauthenticated
|
|
11
|
+
`/preview` endpoint. No API key required, no rate-limit cost, geometry hidden.
|
|
12
|
+
`eolas datasets preview` now uses it instead of burning an authenticated,
|
|
13
|
+
rate-limited `/data` call (DRIFT-5).
|
|
14
|
+
- **`dimensions=` filter on `get()`** and `eolas get --dimensions` — a
|
|
15
|
+
case-insensitive substring filter on dimension columns, applied server-side on
|
|
16
|
+
the live `/data` path (the filter was live in prod but exposed by no client;
|
|
17
|
+
DRIFT-4).
|
|
18
|
+
|
|
6
19
|
## 1.3.22
|
|
7
20
|
|
|
8
21
|
Network-hardening + correctness release, addressing findings from the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: eolas-data
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Python client for the eolas.fyi statistical data API (NZ + OECD; Australia on roadmap)
|
|
5
5
|
Project-URL: Homepage, https://eolas.fyi
|
|
6
6
|
Project-URL: Documentation, https://docs.eolas.fyi/
|
|
@@ -320,14 +320,18 @@ def datasets_info(
|
|
|
320
320
|
def datasets_preview(
|
|
321
321
|
name: str,
|
|
322
322
|
limit: int = typer.Option(
|
|
323
|
-
10, "--limit", "-n", min=1, max=
|
|
323
|
+
10, "--limit", "-n", min=1, max=10, help="Rows to preview (max 10)."
|
|
324
324
|
),
|
|
325
325
|
json_out: bool = typer.Option(False, "--json"),
|
|
326
326
|
api_key: Optional[str] = typer.Option(None, "--api-key"),
|
|
327
327
|
) -> None:
|
|
328
|
-
"""Preview
|
|
328
|
+
"""Preview up to 10 sample rows of a dataset.
|
|
329
|
+
|
|
330
|
+
Uses the unauthenticated ``/preview`` endpoint: no rate-limit cost and no
|
|
331
|
+
live-data query. For real data use ``eolas get``.
|
|
332
|
+
"""
|
|
329
333
|
try:
|
|
330
|
-
df = _client(api_key).
|
|
334
|
+
df = _client(api_key).preview(name, limit=limit)
|
|
331
335
|
except EolasError as e:
|
|
332
336
|
_bail(str(e), _exit_for(e))
|
|
333
337
|
|
|
@@ -368,6 +372,11 @@ def get_cmd(
|
|
|
368
372
|
out: Optional[Path] = typer.Option(
|
|
369
373
|
None, "--out", "-o", help="Write to file. Default: stdout."
|
|
370
374
|
),
|
|
375
|
+
dimensions: Optional[str] = typer.Option(
|
|
376
|
+
None,
|
|
377
|
+
"--dimensions",
|
|
378
|
+
help="Case-insensitive substring filter on dimension columns, e.g. 'auckland'.",
|
|
379
|
+
),
|
|
371
380
|
api_key: Optional[str] = typer.Option(None, "--api-key"),
|
|
372
381
|
) -> None:
|
|
373
382
|
"""Fetch a dataset and write rows to stdout or a file.
|
|
@@ -377,6 +386,7 @@ def get_cmd(
|
|
|
377
386
|
eolas get nz_cpi --format csv > cpi.csv
|
|
378
387
|
eolas get nz_cpi --start 2020-01-01 --format json | jq '.[].value'
|
|
379
388
|
eolas get sa2_2023 --format parquet --out sa2.parquet
|
|
389
|
+
eolas get building_consents --dimensions auckland --format csv
|
|
380
390
|
"""
|
|
381
391
|
fmt = fmt.lower()
|
|
382
392
|
if fmt not in ("csv", "json", "parquet"):
|
|
@@ -388,7 +398,9 @@ def get_cmd(
|
|
|
388
398
|
)
|
|
389
399
|
|
|
390
400
|
try:
|
|
391
|
-
df = _client(api_key).get(
|
|
401
|
+
df = _client(api_key).get(
|
|
402
|
+
name, start=start, end=end, limit=limit, dimensions=dimensions
|
|
403
|
+
)
|
|
392
404
|
except EolasError as e:
|
|
393
405
|
_bail(str(e), _exit_for(e))
|
|
394
406
|
|
|
@@ -2262,6 +2262,36 @@ class Client:
|
|
|
2262
2262
|
# Core data fetch
|
|
2263
2263
|
# ------------------------------------------------------------------
|
|
2264
2264
|
|
|
2265
|
+
def preview(
|
|
2266
|
+
self,
|
|
2267
|
+
name: Union[str, "DatasetName"],
|
|
2268
|
+
*,
|
|
2269
|
+
limit: int = 10,
|
|
2270
|
+
) -> pd.DataFrame:
|
|
2271
|
+
"""Return up to 10 sample rows via the unauthenticated ``/preview`` endpoint.
|
|
2272
|
+
|
|
2273
|
+
A cheap browser-style peek: **no API key required**, it does **not** count
|
|
2274
|
+
against your rate limit, and geometry/WKT columns are hidden server-side
|
|
2275
|
+
(so it stays fast even on large geospatial tables). The server caps the
|
|
2276
|
+
response at 10 rows regardless of ``limit``. For real data use :meth:`get`,
|
|
2277
|
+
:meth:`download`, or :meth:`download_bulk`.
|
|
2278
|
+
|
|
2279
|
+
Args:
|
|
2280
|
+
name: Dataset identifier, e.g. ``"nz_cpi"``.
|
|
2281
|
+
limit: Client-side cap on the returned rows (``<= 10``; the server
|
|
2282
|
+
never returns more than 10).
|
|
2283
|
+
|
|
2284
|
+
Returns:
|
|
2285
|
+
A pandas ``DataFrame`` of the preview rows.
|
|
2286
|
+
"""
|
|
2287
|
+
resp = self._raw_get(f"/v1/datasets/{name}/preview")
|
|
2288
|
+
body = resp.json()
|
|
2289
|
+
rows = body.get("rows", []) if isinstance(body, dict) else body
|
|
2290
|
+
df = pd.DataFrame(rows)
|
|
2291
|
+
if limit and len(df) > limit:
|
|
2292
|
+
df = df.head(limit)
|
|
2293
|
+
return df
|
|
2294
|
+
|
|
2265
2295
|
def get(
|
|
2266
2296
|
self,
|
|
2267
2297
|
name: Union[str, "DatasetName"],
|
|
@@ -2276,6 +2306,7 @@ class Client:
|
|
|
2276
2306
|
envelope: bool = False,
|
|
2277
2307
|
force: bool = False,
|
|
2278
2308
|
progress: ProgressControl = None,
|
|
2309
|
+
dimensions: Optional[str] = None,
|
|
2279
2310
|
) -> Dataset:
|
|
2280
2311
|
"""Fetch dataset rows as a pandas (or polars / geopandas) DataFrame.
|
|
2281
2312
|
|
|
@@ -2321,6 +2352,10 @@ class Client:
|
|
|
2321
2352
|
progress: Control bulk download/read progress when auto-routed to
|
|
2322
2353
|
:meth:`get_local`. ``None`` auto-detects TTY; ignored on the
|
|
2323
2354
|
live API path.
|
|
2355
|
+
dimensions: Case-insensitive substring filter on the dataset's
|
|
2356
|
+
dimension columns (e.g. ``"auckland"``). Applied server-side
|
|
2357
|
+
on the live ``/data`` path; passing it forces the live path
|
|
2358
|
+
(the bulk cache has no per-request dimension filter).
|
|
2324
2359
|
|
|
2325
2360
|
Returns:
|
|
2326
2361
|
A :class:`Dataset` (pandas DataFrame subclass), a polars DataFrame
|
|
@@ -2379,6 +2414,7 @@ class Client:
|
|
|
2379
2414
|
and format == "json"
|
|
2380
2415
|
and not envelope
|
|
2381
2416
|
and not as_arrow
|
|
2417
|
+
and not dimensions # bulk cache has no per-request dimension filter
|
|
2382
2418
|
):
|
|
2383
2419
|
# Narrow the swallow to the ROUTING DECISION only (metadata lookup +
|
|
2384
2420
|
# predicates). A failure there legitimately falls through to the live
|
|
@@ -2427,6 +2463,8 @@ class Client:
|
|
|
2427
2463
|
params["start"] = start
|
|
2428
2464
|
if end:
|
|
2429
2465
|
params["end"] = end
|
|
2466
|
+
if dimensions:
|
|
2467
|
+
params["dimensions"] = dimensions
|
|
2430
2468
|
|
|
2431
2469
|
fetch_limit, user_limit = resolve_fetch_limit(limit)
|
|
2432
2470
|
# Positive limits on large/geo datasets must be sent to the API — the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "eolas-data"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.0"
|
|
8
8
|
description = "Python client for the eolas.fyi statistical data API (NZ + OECD; Australia on roadmap)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Tests for Client.preview() (DRIFT-5) and get(dimensions=) (DRIFT-4)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
import responses as resp_lib
|
|
7
|
+
|
|
8
|
+
from eolas_data import Client
|
|
9
|
+
|
|
10
|
+
BASE = "https://api.eolas.fyi"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture()
|
|
14
|
+
def client():
|
|
15
|
+
return Client("eolas_testkey123", base_url=BASE)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---- DRIFT-5: preview ------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@resp_lib.activate
|
|
22
|
+
def test_preview_hits_unauthenticated_endpoint(client):
|
|
23
|
+
rows = [{"date": "2024-01-01", "value": i} for i in range(10)]
|
|
24
|
+
resp_lib.add(
|
|
25
|
+
resp_lib.GET,
|
|
26
|
+
f"{BASE}/v1/datasets/nz_cpi/preview",
|
|
27
|
+
json={"rows": rows, "hidden_columns": ["geometry_wkt"]},
|
|
28
|
+
)
|
|
29
|
+
df = client.preview("nz_cpi")
|
|
30
|
+
assert len(df) == 10
|
|
31
|
+
assert list(df.columns) == ["date", "value"]
|
|
32
|
+
# It must use /preview, never the rate-limited /data path.
|
|
33
|
+
urls = [c.request.url for c in resp_lib.calls]
|
|
34
|
+
assert any(u.endswith("/nz_cpi/preview") for u in urls)
|
|
35
|
+
assert not any(u.endswith("/data") or "/data?" in u for u in urls)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@resp_lib.activate
|
|
39
|
+
def test_preview_caps_at_limit(client):
|
|
40
|
+
rows = [{"value": i} for i in range(10)]
|
|
41
|
+
resp_lib.add(
|
|
42
|
+
resp_lib.GET,
|
|
43
|
+
f"{BASE}/v1/datasets/nz_cpi/preview",
|
|
44
|
+
json={"rows": rows, "hidden_columns": []},
|
|
45
|
+
)
|
|
46
|
+
assert len(client.preview("nz_cpi", limit=3)) == 3
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---- DRIFT-4: dimensions filter --------------------------------------------
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@resp_lib.activate
|
|
53
|
+
def test_get_passes_dimensions_param(client):
|
|
54
|
+
resp_lib.add(
|
|
55
|
+
resp_lib.GET,
|
|
56
|
+
f"{BASE}/v1/datasets/building_consents/data",
|
|
57
|
+
json={"data": [{"region": "Auckland", "value": 1}]},
|
|
58
|
+
)
|
|
59
|
+
client.get("building_consents", dimensions="auckland")
|
|
60
|
+
data_reqs = [
|
|
61
|
+
c
|
|
62
|
+
for c in resp_lib.calls
|
|
63
|
+
if c.request.url.endswith("/data") or "/data?" in c.request.url
|
|
64
|
+
]
|
|
65
|
+
assert data_reqs, "expected a /data request"
|
|
66
|
+
assert any("dimensions=auckland" in c.request.url for c in data_reqs)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@resp_lib.activate
|
|
70
|
+
def test_dimensions_forces_live_path_no_bulk_route(client):
|
|
71
|
+
# With dimensions set, get() must NOT auto-route to the bulk cache (which has
|
|
72
|
+
# no per-request dimension filter). A /data hit proves the live path ran.
|
|
73
|
+
resp_lib.add(
|
|
74
|
+
resp_lib.GET,
|
|
75
|
+
f"{BASE}/v1/datasets/nz_addresses/data",
|
|
76
|
+
json={"data": [{"suburb": "Ponsonby"}]},
|
|
77
|
+
)
|
|
78
|
+
client.get("nz_addresses", dimensions="ponsonby")
|
|
79
|
+
assert any(
|
|
80
|
+
c.request.url.endswith("/data") or "/data?" in c.request.url
|
|
81
|
+
for c in resp_lib.calls
|
|
82
|
+
)
|
|
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
|
|
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
|