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.
Files changed (48) hide show
  1. {eolas_data-1.3.22 → eolas_data-1.4.0}/CHANGELOG.md +13 -0
  2. {eolas_data-1.3.22 → eolas_data-1.4.0}/PKG-INFO +1 -1
  3. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/__init__.py +1 -1
  4. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/cli.py +16 -4
  5. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/client.py +38 -0
  6. {eolas_data-1.3.22 → eolas_data-1.4.0}/pyproject.toml +1 -1
  7. eolas_data-1.4.0/tests/test_preview_dimensions.py +82 -0
  8. {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/catalog-drift.yml +0 -0
  9. {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/pre-release.yml +0 -0
  10. {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/publish.yml +0 -0
  11. {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/smoke.yml +0 -0
  12. {eolas_data-1.3.22 → eolas_data-1.4.0}/.github/workflows/test.yml +0 -0
  13. {eolas_data-1.3.22 → eolas_data-1.4.0}/.gitignore +0 -0
  14. {eolas_data-1.3.22 → eolas_data-1.4.0}/LICENSE +0 -0
  15. {eolas_data-1.3.22 → eolas_data-1.4.0}/README.md +0 -0
  16. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/_dataset_names.py +0 -0
  17. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/_regen_names.py +0 -0
  18. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/cdc.py +0 -0
  19. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/console.py +0 -0
  20. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/dataset.py +0 -0
  21. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/exceptions.py +0 -0
  22. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/library.py +0 -0
  23. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/meta.py +0 -0
  24. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/rows.py +0 -0
  25. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/schedule.py +0 -0
  26. {eolas_data-1.3.22 → eolas_data-1.4.0}/eolas_data/search.py +0 -0
  27. {eolas_data-1.3.22 → eolas_data-1.4.0}/scripts/check_client_contract.py +0 -0
  28. {eolas_data-1.3.22 → eolas_data-1.4.0}/scripts/preflight.sh +0 -0
  29. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/fixtures/source_helpers.json +0 -0
  30. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_as_arrow.py +0 -0
  31. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_cdc_roundtrip.py +0 -0
  32. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_changes_http.py +0 -0
  33. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_cli.py +0 -0
  34. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_client.py +0 -0
  35. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_date_filter.py +0 -0
  36. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_get_local.py +0 -0
  37. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_keyring.py +0 -0
  38. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_library.py +0 -0
  39. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_meta.py +0 -0
  40. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_progress.py +0 -0
  41. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_resilience.py +0 -0
  42. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_rows.py +0 -0
  43. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_schedule.py +0 -0
  44. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_search.py +0 -0
  45. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_smoke_live.py +0 -0
  46. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_source_helpers.py +0 -0
  47. {eolas_data-1.3.22 → eolas_data-1.4.0}/tests/test_sync_bulk.py +0 -0
  48. {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.22
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/
@@ -26,7 +26,7 @@ from .exceptions import (
26
26
  try:
27
27
  __version__ = _pkg_version("eolas-data")
28
28
  except PackageNotFoundError: # pragma: no cover - uninstalled source checkout
29
- __version__ = "1.3.22"
29
+ __version__ = "1.4.0"
30
30
 
31
31
  __all__ = [
32
32
  "Client",
@@ -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=1000, help="Rows to preview."
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 the most recent N rows of a dataset."""
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).get(name, limit=limit)
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(name, start=start, end=end, limit=limit)
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.3.22"
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