eolas-data 1.3.4__tar.gz → 1.3.5__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 (37) hide show
  1. {eolas_data-1.3.4 → eolas_data-1.3.5}/PKG-INFO +2 -2
  2. {eolas_data-1.3.4 → eolas_data-1.3.5}/README.md +1 -1
  3. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/client.py +110 -51
  4. {eolas_data-1.3.4 → eolas_data-1.3.5}/pyproject.toml +1 -1
  5. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_progress.py +19 -0
  6. {eolas_data-1.3.4 → eolas_data-1.3.5}/.github/workflows/catalog-drift.yml +0 -0
  7. {eolas_data-1.3.4 → eolas_data-1.3.5}/.github/workflows/publish.yml +0 -0
  8. {eolas_data-1.3.4 → eolas_data-1.3.5}/.github/workflows/smoke.yml +0 -0
  9. {eolas_data-1.3.4 → eolas_data-1.3.5}/.github/workflows/test.yml +0 -0
  10. {eolas_data-1.3.4 → eolas_data-1.3.5}/.gitignore +0 -0
  11. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/__init__.py +0 -0
  12. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/_dataset_names.py +0 -0
  13. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/_regen_names.py +0 -0
  14. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/cdc.py +0 -0
  15. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/cli.py +0 -0
  16. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/console.py +0 -0
  17. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/dataset.py +0 -0
  18. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/exceptions.py +0 -0
  19. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/library.py +0 -0
  20. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/meta.py +0 -0
  21. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/rows.py +0 -0
  22. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/schedule.py +0 -0
  23. {eolas_data-1.3.4 → eolas_data-1.3.5}/eolas_data/search.py +0 -0
  24. {eolas_data-1.3.4 → eolas_data-1.3.5}/scripts/preflight.sh +0 -0
  25. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_as_arrow.py +0 -0
  26. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_cdc_roundtrip.py +0 -0
  27. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_cli.py +0 -0
  28. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_client.py +0 -0
  29. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_keyring.py +0 -0
  30. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_library.py +0 -0
  31. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_meta.py +0 -0
  32. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_rows.py +0 -0
  33. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_schedule.py +0 -0
  34. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_search.py +0 -0
  35. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_smoke_live.py +0 -0
  36. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_sync_bulk.py +0 -0
  37. {eolas_data-1.3.4 → eolas_data-1.3.5}/tests/test_sync_changes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eolas-data
3
- Version: 1.3.4
3
+ Version: 1.3.5
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/
@@ -231,7 +231,7 @@ r = client.sync_bulk("nz_cpi", path="nz_cpi.parquet")
231
231
  path = client.download_bulk("treasury_fiscal_spending", path="t.parquet")
232
232
  ```
233
233
 
234
- **Progress bars:** `download_bulk`, `sync_bulk`, and `get_local` all show a `tqdm` progress bar automatically in interactive terminals and VSCode notebooks, so 1+ GB files are never silent. Pass `progress=False` to suppress in scripts, or set `EOLAS_NO_PROGRESS=1` in the environment for a CI-wide escape hatch. The `--no-progress` flag does the same from the CLI.
234
+ **Progress bars:** `get_local()` shows two phases in interactive sessions — a **download** byte bar while fetching from CDN, then a **read** spinner while Parquet/GeoParquet is loaded (often the slow part on multi-million-row geo datasets). Control with `progress=True` (both), `False` (neither), `"download"`, or `"read"`. Set `EOLAS_NO_PROGRESS=1` to suppress both in batch scripts. Cached files skip the download bar and print an informative message instead.
235
235
 
236
236
  CLI mirror: `eolas download <name>` for one-shot, `eolas sync <name> [--watch hourly]` for an incremental check. Full docs: [docs.eolas.fyi/bulk-downloads/](https://docs.eolas.fyi/bulk-downloads/).
237
237
 
@@ -183,7 +183,7 @@ r = client.sync_bulk("nz_cpi", path="nz_cpi.parquet")
183
183
  path = client.download_bulk("treasury_fiscal_spending", path="t.parquet")
184
184
  ```
185
185
 
186
- **Progress bars:** `download_bulk`, `sync_bulk`, and `get_local` all show a `tqdm` progress bar automatically in interactive terminals and VSCode notebooks, so 1+ GB files are never silent. Pass `progress=False` to suppress in scripts, or set `EOLAS_NO_PROGRESS=1` in the environment for a CI-wide escape hatch. The `--no-progress` flag does the same from the CLI.
186
+ **Progress bars:** `get_local()` shows two phases in interactive sessions — a **download** byte bar while fetching from CDN, then a **read** spinner while Parquet/GeoParquet is loaded (often the slow part on multi-million-row geo datasets). Control with `progress=True` (both), `False` (neither), `"download"`, or `"read"`. Set `EOLAS_NO_PROGRESS=1` to suppress both in batch scripts. Cached files skip the download bar and print an informative message instead.
187
187
 
188
188
  CLI mirror: `eolas download <name>` for one-shot, `eolas sync <name> [--watch hourly]` for an incremental check. Full docs: [docs.eolas.fyi/bulk-downloads/](https://docs.eolas.fyi/bulk-downloads/).
189
189
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  import datetime
4
5
  import json
5
6
  import logging
@@ -7,7 +8,10 @@ import os
7
8
  import pathlib
8
9
  import sys
9
10
  from dataclasses import dataclass
10
- from typing import Optional, Union
11
+ from typing import Literal, Optional, Union
12
+
13
+ ProgressControl = Union[bool, str, None]
14
+ ProgressPhase = Literal["download", "read"]
11
15
 
12
16
  import pandas as pd
13
17
  import requests
@@ -362,7 +366,7 @@ class Client:
362
366
  freshness: str = "auto",
363
367
  format: str = "parquet",
364
368
  path: Optional[Union[str, "pathlib.Path"]] = None,
365
- progress: Optional[bool] = None,
369
+ progress: ProgressControl = None,
366
370
  ) -> "Union[pathlib.Path, bytes]":
367
371
  """Download a complete dataset as a single binary file.
368
372
 
@@ -387,14 +391,10 @@ class Client:
387
391
  bytes. Pass a ``str`` or ``pathlib.Path`` to write to disk and
388
392
  return the resolved path. Parent directories are created if
389
393
  needed.
390
- progress: Control the download progress bar.
391
- ``None`` (default) auto-detects: bar shown when
392
- ``sys.stdout.isatty()`` is True (interactive terminal or
393
- notebook), hidden when piped or in CI.
394
- ``True`` forces the bar on regardless (useful in log-tailing
395
- scenarios). ``False`` forces it off. When ``path`` is
396
- ``None`` (bytes mode) progress is always disabled — there is
397
- no file path to label.
394
+ progress: Control the download progress bar (``"download"`` phase).
395
+ See :meth:`get_local` for the full selector vocabulary
396
+ (``"read"`` applies only to :meth:`get_local`). When ``path``
397
+ is ``None`` (bytes mode) progress is always disabled.
398
398
 
399
399
  Returns:
400
400
  ``pathlib.Path`` when ``path`` is set (the resolved, written path).
@@ -467,13 +467,13 @@ class Client:
467
467
  out = pathlib.Path(path).expanduser().resolve()
468
468
  out.parent.mkdir(parents=True, exist_ok=True)
469
469
 
470
- show = self._resolve_show_progress(progress)
470
+ show = self._resolve_show_progress(progress, "download")
471
471
  resp = self._raw_bulk_get(bulk_path, params=params, stream=True)
472
472
  total = int(resp.headers.get("Content-Length", 0)) or None
473
473
  self._stream_to_file_with_progress(
474
474
  resp, out,
475
475
  total_bytes=total,
476
- label=out.name,
476
+ label=f"Downloading {out.name}",
477
477
  show_progress=show,
478
478
  )
479
479
  return out
@@ -485,7 +485,7 @@ class Client:
485
485
  path: Union[str, "pathlib.Path"],
486
486
  format: str = "parquet",
487
487
  freshness: str = "auto",
488
- progress: Optional[bool] = None,
488
+ progress: ProgressControl = None,
489
489
  ) -> SyncResult:
490
490
  """Incrementally sync a bulk dataset file — only re-download when the snapshot changes.
491
491
 
@@ -514,11 +514,10 @@ class Client:
514
514
  ``"geoparquet"``.
515
515
  freshness: ``"auto"`` (default), ``"monthly"``, or ``"current"``.
516
516
  Passed verbatim to the bulk endpoint.
517
- progress: Control the download progress bar.
518
- ``None`` (default) auto-detects via ``sys.stdout.isatty()``.
519
- ``True`` forces the bar on; ``False`` forces it off.
520
- When ``status="unchanged"`` no data is transferred so no bar
521
- is shown regardless of this setting.
517
+ progress: Control the download progress bar (``"download"`` phase).
518
+ See :meth:`get_local` for the full selector vocabulary.
519
+ When ``status="unchanged"`` no download bar is shown; an
520
+ informative cached-file message is printed instead.
522
521
 
523
522
  Returns:
524
523
  A :class:`SyncResult` dataclass with ``status``,
@@ -606,6 +605,7 @@ class Client:
606
605
  and prev.get("snapshot_id") == current_sid
607
606
  and out.exists()
608
607
  ):
608
+ print(f"Using cached {out.name} (up to date).", file=sys.stderr)
609
609
  return SyncResult(
610
610
  status="unchanged",
611
611
  previous_snapshot_id=prev.get("snapshot_id"),
@@ -617,14 +617,14 @@ class Client:
617
617
  # Download (atomic replace).
618
618
  out.parent.mkdir(parents=True, exist_ok=True)
619
619
  tmp = out.with_suffix(out.suffix + f".eolas-tmp-{os.urandom(4).hex()}")
620
- show = self._resolve_show_progress(progress)
620
+ show = self._resolve_show_progress(progress, "download")
621
621
  try:
622
622
  resp = self._raw_bulk_get(bulk_path, params=params, stream=True)
623
623
  total = int(resp.headers.get("Content-Length", 0)) or None
624
624
  bytes_dl = self._stream_to_file_with_progress(
625
625
  resp, tmp,
626
626
  total_bytes=total,
627
- label=out.name,
627
+ label=f"Downloading {out.name}",
628
628
  show_progress=show,
629
629
  )
630
630
  if bytes_dl == 0:
@@ -1159,28 +1159,73 @@ class Client:
1159
1159
  # ------------------------------------------------------------------
1160
1160
 
1161
1161
  @staticmethod
1162
- def _resolve_show_progress(progress: Optional[bool]) -> bool:
1163
- """Resolve the tri-state ``progress`` kwarg to a concrete bool.
1164
-
1165
- Priority (highest first):
1166
- 1. Explicit ``progress=True/False`` kwarg from the caller.
1167
- 2. ``EOLAS_NO_PROGRESS=1`` environment variable — always suppresses.
1168
- 3. Jupyter / IPython kernel detection — Jupyter wraps stdout in an
1169
- ``OutStream``, so ``sys.stdout.isatty()`` returns False even when
1170
- the user is clearly in an interactive notebook session. If
1171
- ``ipykernel`` is loaded (the standard signal used by tqdm itself),
1172
- we're in a notebook → show the bar; ``tqdm.auto`` then renders it
1173
- as an ipywidget (or text bar as fallback).
1174
- 4. ``sys.stdout.isatty()`` auto-detection for plain terminals.
1175
- """
1176
- if progress is not None:
1177
- return bool(progress)
1162
+ def _progress_auto_detect() -> bool:
1163
+ """Whether progress feedback should show when ``progress`` is None."""
1178
1164
  if os.getenv("EOLAS_NO_PROGRESS", "").strip() in ("1", "true", "yes"):
1179
1165
  return False
1180
1166
  if "ipykernel" in sys.modules:
1181
1167
  return True
1182
1168
  return sys.stdout.isatty()
1183
1169
 
1170
+ @staticmethod
1171
+ def _resolve_progress_phases(progress: ProgressControl) -> dict[str, bool]:
1172
+ """Map ``progress`` to download/read phase flags.
1173
+
1174
+ Accepts ``None``, ``True``/``False``, or ``"both"``, ``"download"``,
1175
+ ``"read"``, ``"none"`` (and ``"all"`` as alias for ``"both"``).
1176
+ """
1177
+ if progress is False:
1178
+ return {"download": False, "read": False}
1179
+ if progress is True:
1180
+ return {"download": True, "read": True}
1181
+ if isinstance(progress, str):
1182
+ key = progress.strip().lower()
1183
+ table = {
1184
+ "both": (True, True),
1185
+ "all": (True, True),
1186
+ "download": (True, False),
1187
+ "read": (False, True),
1188
+ "none": (False, False),
1189
+ }
1190
+ if key not in table:
1191
+ raise ValueError(
1192
+ "progress must be True, False, None, or one of "
1193
+ "'both', 'download', 'read', 'none'."
1194
+ )
1195
+ d, r = table[key]
1196
+ return {"download": d, "read": r}
1197
+ auto = Client._progress_auto_detect()
1198
+ return {"download": auto, "read": auto}
1199
+
1200
+ @staticmethod
1201
+ def _resolve_show_progress(
1202
+ progress: ProgressControl,
1203
+ phase: ProgressPhase = "download",
1204
+ ) -> bool:
1205
+ """Resolve ``progress`` for one bulk phase (download or read)."""
1206
+ return Client._resolve_progress_phases(progress)[phase]
1207
+
1208
+ @staticmethod
1209
+ @contextlib.contextmanager
1210
+ def _with_read_progress(label: str, show: bool):
1211
+ """Indeterminate spinner while materialising a cached bulk file."""
1212
+ if not show:
1213
+ yield
1214
+ return
1215
+ import tqdm.auto
1216
+
1217
+ bar = tqdm.auto.tqdm(
1218
+ total=1,
1219
+ desc=f"Loading {label} from disk",
1220
+ leave=False,
1221
+ bar_format="{desc}…",
1222
+ )
1223
+ try:
1224
+ yield
1225
+ finally:
1226
+ bar.update(1)
1227
+ bar.close()
1228
+
1184
1229
  @staticmethod
1185
1230
  def _stream_to_file_with_progress(
1186
1231
  resp: "requests.Response",
@@ -1796,7 +1841,7 @@ class Client:
1796
1841
  as_geo: Optional[bool] = None,
1797
1842
  as_arrow: bool = False,
1798
1843
  meta: bool = True,
1799
- progress: Optional[bool] = None,
1844
+ progress: ProgressControl = None,
1800
1845
  ) -> "pd.DataFrame":
1801
1846
  """Download (or serve from cache) a whole dataset as a local DataFrame.
1802
1847
 
@@ -1833,7 +1878,14 @@ class Client:
1833
1878
  meta: When ``True`` (default), attach table/column metadata from
1834
1879
  :meth:`info` (session-cached). Pass ``False`` to skip the extra
1835
1880
  round-trip.
1836
- progress: Forwarded to :meth:`sync_bulk` (``None`` auto-detects TTY).
1881
+ progress: Control progress for two phases: **download** (streaming
1882
+ byte bar via :meth:`sync_bulk`) and **read** (spinner while
1883
+ Parquet/GeoParquet is loaded into a DataFrame). ``None``
1884
+ (default) enables both in interactive sessions. ``True``/``False``
1885
+ force both on/off. Use ``"download"``, ``"read"``, ``"both"``,
1886
+ or ``"none"`` for one phase only. Suppressed by
1887
+ ``EOLAS_NO_PROGRESS=1``. Cached snapshots skip the download bar
1888
+ and print an informative message instead.
1837
1889
 
1838
1890
  Returns:
1839
1891
  ``pd.DataFrame`` (tabular) or ``geopandas.GeoDataFrame`` (geo +
@@ -1896,24 +1948,31 @@ class Client:
1896
1948
  # ---- sync (download if needed, HEAD check if cached) -----------------
1897
1949
  self.sync_bulk(name, path=file_path, format=fmt, freshness=freshness, progress=progress)
1898
1950
 
1951
+ show_read = self._resolve_show_progress(progress, "read")
1952
+ read_label = file_path.name
1953
+
1899
1954
  # ---- read the local file into a DataFrame ----------------------------
1900
1955
  if as_arrow:
1901
1956
  import pyarrow.parquet as _pq
1902
- if fmt == "csv_gz":
1903
- import pyarrow as _pa
1904
- return _pa.Table.from_pandas(pd.read_csv(file_path), preserve_index=False)
1905
- return _pq.read_table(file_path)
1957
+ with self._with_read_progress(read_label, show_read):
1958
+ if fmt == "csv_gz":
1959
+ import pyarrow as _pa
1960
+ return _pa.Table.from_pandas(
1961
+ pd.read_csv(file_path), preserve_index=False,
1962
+ )
1963
+ return _pq.read_table(file_path)
1906
1964
 
1907
1965
  def _read_bulk_file(path: pathlib.Path, bulk_fmt: str):
1908
- if bulk_fmt == "csv_gz":
1909
- return pd.read_csv(path)
1910
- if bulk_fmt == "geoparquet" and resolved_as_geo:
1911
- try:
1912
- import geopandas as gpd
1913
- return gpd.read_parquet(path)
1914
- except ImportError:
1915
- pass
1916
- return pd.read_parquet(path)
1966
+ with self._with_read_progress(read_label, show_read):
1967
+ if bulk_fmt == "csv_gz":
1968
+ return pd.read_csv(path)
1969
+ if bulk_fmt == "geoparquet" and resolved_as_geo:
1970
+ try:
1971
+ import geopandas as gpd
1972
+ return gpd.read_parquet(path)
1973
+ except ImportError:
1974
+ pass
1975
+ return pd.read_parquet(path)
1917
1976
 
1918
1977
  try:
1919
1978
  result = _read_bulk_file(file_path, fmt)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "eolas-data"
7
- version = "1.3.4"
7
+ version = "1.3.5"
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" }
@@ -421,6 +421,25 @@ def test_eolas_no_progress_env_suppresses_bar(client, tmp_path, monkeypatch):
421
421
  assert all(disabled_values), "EOLAS_NO_PROGRESS=1 must disable tqdm even when isatty=True"
422
422
 
423
423
 
424
+ def test_progress_phase_selectors():
425
+ from eolas_data.client import Client
426
+
427
+ phases = Client._resolve_progress_phases("download")
428
+ assert phases == {"download": True, "read": False}
429
+
430
+ phases = Client._resolve_progress_phases("read")
431
+ assert phases == {"download": False, "read": True}
432
+
433
+ phases = Client._resolve_progress_phases("both")
434
+ assert phases == {"download": True, "read": True}
435
+
436
+ phases = Client._resolve_progress_phases("none")
437
+ assert phases == {"download": False, "read": False}
438
+
439
+ assert Client._resolve_show_progress("read", "read") is True
440
+ assert Client._resolve_show_progress("download", "read") is False
441
+
442
+
424
443
  def test_progress_resolver_detects_jupyter():
425
444
  """Jupyter wraps stdout so isatty() is False, but the user IS interactive
426
445
  and tqdm.auto can render a widget. Resolver must return True when
File without changes
File without changes
File without changes
File without changes