eolas-data 1.0.0__py3-none-any.whl
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/__init__.py +16 -0
- eolas_data/_dataset_names.py +2833 -0
- eolas_data/_regen_names.py +57 -0
- eolas_data/cli.py +637 -0
- eolas_data/client.py +680 -0
- eolas_data/dataset.py +46 -0
- eolas_data/exceptions.py +20 -0
- eolas_data/schedule.py +258 -0
- eolas_data-1.0.0.dist-info/METADATA +203 -0
- eolas_data-1.0.0.dist-info/RECORD +12 -0
- eolas_data-1.0.0.dist-info/WHEEL +4 -0
- eolas_data-1.0.0.dist-info/entry_points.txt +2 -0
eolas_data/dataset.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Dataset(pd.DataFrame):
|
|
7
|
+
"""A pandas DataFrame with eolas dataset metadata.
|
|
8
|
+
|
|
9
|
+
Behaves exactly like a DataFrame — all pandas operations work normally.
|
|
10
|
+
Extra attributes:
|
|
11
|
+
eolas_name: Dataset identifier (e.g. ``"nz_cpi"``).
|
|
12
|
+
eolas_source: Data source label (e.g. ``"Stats NZ"``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
_metadata = ["eolas_name", "eolas_source"]
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _constructor(self):
|
|
19
|
+
return Dataset
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
name = getattr(self, "eolas_name", "") or ""
|
|
23
|
+
source = getattr(self, "eolas_source", "") or ""
|
|
24
|
+
if name:
|
|
25
|
+
header = f"# Dataset: {name}"
|
|
26
|
+
if source:
|
|
27
|
+
header += f" [{source}]"
|
|
28
|
+
header += f"\n# {len(self)} rows\n"
|
|
29
|
+
return header + pd.DataFrame.__repr__(self)
|
|
30
|
+
return pd.DataFrame.__repr__(self)
|
|
31
|
+
|
|
32
|
+
# ------------------------------------------------------------------
|
|
33
|
+
# plot_dataset() was removed in v1.3.0.
|
|
34
|
+
#
|
|
35
|
+
# It auto-picked `date` and `value` columns and drew a single matplotlib
|
|
36
|
+
# line — but datasets with a dimension column (multiple series per date)
|
|
37
|
+
# produced silent zigzag traces. Rather than ship a helper that has to
|
|
38
|
+
# know each dataset's shape, plotting is now the caller's responsibility.
|
|
39
|
+
# `Dataset` subclasses `DataFrame`, so any matplotlib / seaborn / plotly
|
|
40
|
+
# workflow works straight out of the box:
|
|
41
|
+
#
|
|
42
|
+
# import matplotlib.pyplot as plt
|
|
43
|
+
# df.plot(x="date", y="value")
|
|
44
|
+
#
|
|
45
|
+
# See README for one-liners.
|
|
46
|
+
# ------------------------------------------------------------------
|
eolas_data/exceptions.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
class EolasError(Exception):
|
|
2
|
+
"""Base exception for the eolas-data client."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AuthenticationError(EolasError):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RateLimitError(EolasError):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class NotFoundError(EolasError):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class APIError(EolasError):
|
|
18
|
+
def __init__(self, status_code: int, message: str):
|
|
19
|
+
self.status_code = status_code
|
|
20
|
+
super().__init__(f"HTTP {status_code}: {message}")
|
eolas_data/schedule.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""Cross-platform scheduling backend for `eolas schedule add|list|remove`.
|
|
2
|
+
|
|
3
|
+
POSIX (Linux/macOS): edits the user's crontab via `crontab -l` / `crontab -`.
|
|
4
|
+
Windows: uses `schtasks` to create per-user scheduled tasks.
|
|
5
|
+
|
|
6
|
+
Both backends only manage entries tagged with a sentinel so the user's other
|
|
7
|
+
cron jobs / scheduled tasks are never touched.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import csv
|
|
12
|
+
import io
|
|
13
|
+
import platform
|
|
14
|
+
import re
|
|
15
|
+
import shlex
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
SENTINEL = "# eolas-schedule:"
|
|
22
|
+
TASK_PREFIX = "eolas-" # Windows task name prefix
|
|
23
|
+
|
|
24
|
+
# Interval shortcut → cron expression (minute hour dom month dow). Daily/weekly/
|
|
25
|
+
# monthly all default to 6am because datasets typically refresh in the early
|
|
26
|
+
# hours; running at 6am gets the freshest data without competing for resources.
|
|
27
|
+
INTERVALS = {
|
|
28
|
+
"hourly": "0 * * * *",
|
|
29
|
+
"daily": "0 6 * * *",
|
|
30
|
+
"weekly": "0 6 * * 1", # Monday 6am
|
|
31
|
+
"monthly": "0 6 1 * *", # 1st of month, 6am
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Windows schtasks /sc value per interval. Custom cron exprs not supported on
|
|
35
|
+
# Windows backend — see _windows_add for the fallback message.
|
|
36
|
+
WIN_SCHED = {
|
|
37
|
+
"hourly": ("HOURLY", None),
|
|
38
|
+
"daily": ("DAILY", "06:00"),
|
|
39
|
+
"weekly": ("WEEKLY", "06:00"), # default day = today's weekday; we override below
|
|
40
|
+
"monthly": ("MONTHLY", "06:00"),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
CRON_EXPR_RE = re.compile(r"^\s*\S+\s+\S+\s+\S+\s+\S+\s+\S+\s*$")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ScheduleEntry:
|
|
48
|
+
name: str
|
|
49
|
+
schedule: str # cron expr (POSIX) or human description (Windows)
|
|
50
|
+
command: str
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
# Public API — dispatches per OS
|
|
55
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def is_windows() -> bool:
|
|
58
|
+
return platform.system() == "Windows"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add(name: str, schedule_expr: str, command: str) -> None:
|
|
62
|
+
"""Register a scheduled task. `schedule_expr` is a cron expression on POSIX
|
|
63
|
+
or one of {'hourly','daily','weekly','monthly'} on Windows."""
|
|
64
|
+
if is_windows():
|
|
65
|
+
_windows_add(name, schedule_expr, command)
|
|
66
|
+
else:
|
|
67
|
+
_cron_add(name, schedule_expr, command)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove(name: str) -> bool:
|
|
71
|
+
"""Remove a managed task. Returns True if removed, False if not found."""
|
|
72
|
+
if is_windows():
|
|
73
|
+
return _windows_remove(name)
|
|
74
|
+
return _cron_remove(name)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_entries() -> list[ScheduleEntry]:
|
|
78
|
+
"""Return all managed eolas-schedule entries."""
|
|
79
|
+
if is_windows():
|
|
80
|
+
return _windows_list()
|
|
81
|
+
return _cron_list()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def interval_to_cron(interval: str) -> str:
|
|
85
|
+
"""Return the cron expression for an interval shortcut. Raises on unknown."""
|
|
86
|
+
if interval not in INTERVALS:
|
|
87
|
+
raise ValueError(f"unknown interval {interval!r}; expected one of {list(INTERVALS)}")
|
|
88
|
+
return INTERVALS[interval]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def validate_cron_expr(expr: str) -> None:
|
|
92
|
+
"""Basic shape check on a 5-field cron expression. Raises on invalid."""
|
|
93
|
+
if not CRON_EXPR_RE.match(expr):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"invalid cron expression {expr!r}; expected 5 fields "
|
|
96
|
+
"(minute hour day-of-month month day-of-week)"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
# POSIX cron backend
|
|
102
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def _crontab_available() -> bool:
|
|
105
|
+
return shutil.which("crontab") is not None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _cron_read() -> list[str]:
|
|
109
|
+
"""Read the user's crontab. Returns [] when no crontab is set."""
|
|
110
|
+
if not _crontab_available():
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
"crontab is not installed on this system. "
|
|
113
|
+
"On Debian/Ubuntu: sudo apt-get install cron. On Alpine: apk add busybox-suid."
|
|
114
|
+
)
|
|
115
|
+
proc = subprocess.run(
|
|
116
|
+
["crontab", "-l"], capture_output=True, text=True
|
|
117
|
+
)
|
|
118
|
+
if proc.returncode == 0:
|
|
119
|
+
return proc.stdout.splitlines()
|
|
120
|
+
# Some implementations exit 1 with "no crontab" — treat as empty.
|
|
121
|
+
if "no crontab" in (proc.stderr or "").lower():
|
|
122
|
+
return []
|
|
123
|
+
raise RuntimeError(f"crontab -l failed: {proc.stderr.strip() or proc.stdout.strip()}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _cron_write(lines: list[str]) -> None:
|
|
127
|
+
payload = "\n".join(lines).rstrip() + "\n"
|
|
128
|
+
proc = subprocess.run(
|
|
129
|
+
["crontab", "-"], input=payload, text=True, capture_output=True
|
|
130
|
+
)
|
|
131
|
+
if proc.returncode != 0:
|
|
132
|
+
raise RuntimeError(f"crontab - failed: {proc.stderr.strip()}")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _cron_format_line(name: str, cron_expr: str, command: str) -> str:
|
|
136
|
+
return f"{cron_expr} {command} {SENTINEL} {name}"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _cron_match_name(line: str, name: str) -> bool:
|
|
140
|
+
return SENTINEL in line and line.rstrip().endswith(name)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _cron_add(name: str, cron_expr: str, command: str) -> None:
|
|
144
|
+
validate_cron_expr(cron_expr)
|
|
145
|
+
lines = [l for l in _cron_read() if not _cron_match_name(l, name)] # idempotent
|
|
146
|
+
lines.append(_cron_format_line(name, cron_expr, command))
|
|
147
|
+
_cron_write(lines)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _cron_remove(name: str) -> bool:
|
|
151
|
+
lines = _cron_read()
|
|
152
|
+
kept = [l for l in lines if not _cron_match_name(l, name)]
|
|
153
|
+
if len(kept) == len(lines):
|
|
154
|
+
return False
|
|
155
|
+
_cron_write(kept)
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _cron_list() -> list[ScheduleEntry]:
|
|
160
|
+
out: list[ScheduleEntry] = []
|
|
161
|
+
for line in _cron_read():
|
|
162
|
+
if SENTINEL not in line:
|
|
163
|
+
continue
|
|
164
|
+
head, _, tail = line.partition(SENTINEL)
|
|
165
|
+
name = tail.strip()
|
|
166
|
+
parts = head.strip().split(maxsplit=5)
|
|
167
|
+
if len(parts) < 6:
|
|
168
|
+
continue # malformed; skip silently
|
|
169
|
+
cron_expr = " ".join(parts[:5])
|
|
170
|
+
command = parts[5]
|
|
171
|
+
out.append(ScheduleEntry(name=name, schedule=cron_expr, command=command))
|
|
172
|
+
return out
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
# Windows schtasks backend
|
|
177
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
def _schtasks_available() -> bool:
|
|
180
|
+
return shutil.which("schtasks") is not None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _windows_add(name: str, interval: str, command: str) -> None:
|
|
184
|
+
if not _schtasks_available():
|
|
185
|
+
raise RuntimeError("schtasks not found — required on Windows for scheduling")
|
|
186
|
+
if interval not in WIN_SCHED:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"Windows backend supports interval shortcuts only "
|
|
189
|
+
f"({list(WIN_SCHED)}); got {interval!r}. "
|
|
190
|
+
"Custom cron expressions aren't translatable; use schtasks GUI for advanced cases."
|
|
191
|
+
)
|
|
192
|
+
sc, st = WIN_SCHED[interval]
|
|
193
|
+
args = [
|
|
194
|
+
"schtasks", "/create",
|
|
195
|
+
"/tn", f"{TASK_PREFIX}{name}",
|
|
196
|
+
"/tr", command,
|
|
197
|
+
"/sc", sc,
|
|
198
|
+
"/f", # overwrite if exists (idempotent add)
|
|
199
|
+
]
|
|
200
|
+
if st:
|
|
201
|
+
args += ["/st", st]
|
|
202
|
+
if interval == "weekly":
|
|
203
|
+
args += ["/d", "MON"]
|
|
204
|
+
proc = subprocess.run(args, capture_output=True, text=True)
|
|
205
|
+
if proc.returncode != 0:
|
|
206
|
+
raise RuntimeError(f"schtasks /create failed: {proc.stderr.strip()}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _windows_remove(name: str) -> bool:
|
|
210
|
+
proc = subprocess.run(
|
|
211
|
+
["schtasks", "/delete", "/tn", f"{TASK_PREFIX}{name}", "/f"],
|
|
212
|
+
capture_output=True, text=True,
|
|
213
|
+
)
|
|
214
|
+
if proc.returncode == 0:
|
|
215
|
+
return True
|
|
216
|
+
# schtasks returns non-zero if the task doesn't exist
|
|
217
|
+
if "cannot find" in (proc.stderr + proc.stdout).lower():
|
|
218
|
+
return False
|
|
219
|
+
raise RuntimeError(f"schtasks /delete failed: {proc.stderr.strip()}")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _windows_list() -> list[ScheduleEntry]:
|
|
223
|
+
proc = subprocess.run(
|
|
224
|
+
["schtasks", "/query", "/fo", "CSV", "/v"],
|
|
225
|
+
capture_output=True, text=True,
|
|
226
|
+
)
|
|
227
|
+
if proc.returncode != 0:
|
|
228
|
+
raise RuntimeError(f"schtasks /query failed: {proc.stderr.strip()}")
|
|
229
|
+
out: list[ScheduleEntry] = []
|
|
230
|
+
reader = csv.DictReader(io.StringIO(proc.stdout))
|
|
231
|
+
for row in reader:
|
|
232
|
+
task_name = (row.get("TaskName") or "").lstrip("\\").strip()
|
|
233
|
+
if not task_name.startswith(TASK_PREFIX):
|
|
234
|
+
continue
|
|
235
|
+
out.append(ScheduleEntry(
|
|
236
|
+
name=task_name[len(TASK_PREFIX):],
|
|
237
|
+
schedule=row.get("Schedule Type") or "",
|
|
238
|
+
command=row.get("Task To Run") or "",
|
|
239
|
+
))
|
|
240
|
+
return out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
# Helpers used by cli.py
|
|
245
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
def build_command(eolas_path: str, dataset: str, out_path: str,
|
|
248
|
+
start: Optional[str] = None, end: Optional[str] = None,
|
|
249
|
+
fmt: str = "csv") -> str:
|
|
250
|
+
"""Construct the shell command line to put inside the cron entry."""
|
|
251
|
+
parts = [shlex.quote(eolas_path), "get", shlex.quote(dataset),
|
|
252
|
+
"--format", shlex.quote(fmt),
|
|
253
|
+
"--out", shlex.quote(str(out_path))]
|
|
254
|
+
if start:
|
|
255
|
+
parts += ["--start", shlex.quote(start)]
|
|
256
|
+
if end:
|
|
257
|
+
parts += ["--end", shlex.quote(end)]
|
|
258
|
+
return " ".join(parts)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eolas-data
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the eolas.fyi statistical data API (NZ, Australia, OECD)
|
|
5
|
+
Project-URL: Homepage, https://eolas.fyi
|
|
6
|
+
Project-URL: Documentation, https://docs.eolas.fyi/
|
|
7
|
+
Project-URL: Repository, https://github.com/phildonovan/eolas-data
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/phildonovan/eolas-data/issues
|
|
9
|
+
Author-email: Virtus Solutions <phil@virtus-solutions.io>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: api,australia,economics,eolas,new-zealand,statistics
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: pandas>=1.5
|
|
24
|
+
Requires-Dist: requests>=2.28
|
|
25
|
+
Provides-Extra: cli
|
|
26
|
+
Requires-Dist: rich>=13; extra == 'cli'
|
|
27
|
+
Requires-Dist: typer>=0.12; extra == 'cli'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: geopandas>=0.14; extra == 'dev'
|
|
30
|
+
Requires-Dist: pandas; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
32
|
+
Requires-Dist: responses; extra == 'dev'
|
|
33
|
+
Requires-Dist: rich>=13; extra == 'dev'
|
|
34
|
+
Requires-Dist: shapely>=2.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: typer>=0.12; extra == 'dev'
|
|
36
|
+
Provides-Extra: geo
|
|
37
|
+
Requires-Dist: geopandas>=0.14; extra == 'geo'
|
|
38
|
+
Requires-Dist: shapely>=2.0; extra == 'geo'
|
|
39
|
+
Provides-Extra: polars
|
|
40
|
+
Requires-Dist: polars>=0.20; extra == 'polars'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# eolas-data
|
|
44
|
+
|
|
45
|
+
Python client for the [eolas.fyi](https://eolas.fyi) statistical data API — 717+ datasets across NZ, Australia, OECD, and more, served as tidy `pandas` DataFrames (or `polars` / `geopandas` if you prefer).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install eolas-data
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from eolas_data import Client
|
|
55
|
+
|
|
56
|
+
client = Client("your_api_key") # or set EOLAS_API_KEY in env
|
|
57
|
+
|
|
58
|
+
# Generic
|
|
59
|
+
df = client.get("nz_cpi", start="2020-01-01")
|
|
60
|
+
|
|
61
|
+
# Source-specific (sets the `eolas_source` metadata)
|
|
62
|
+
df = client.statsnz("nz_cpi")
|
|
63
|
+
df = client.oecd("nz_gdp_production_annual")
|
|
64
|
+
|
|
65
|
+
# Discovery
|
|
66
|
+
all_datasets = client.list()
|
|
67
|
+
nz_only = client.list("Stats NZ")
|
|
68
|
+
meta = client.info("nz_cpi")
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Get an API key at <https://eolas.fyi/signup>. Free plan is 10 requests/month; Starter is 100; Pro is unlimited.
|
|
72
|
+
|
|
73
|
+
## Command-line interface
|
|
74
|
+
|
|
75
|
+
`pip install eolas-data[cli]` adds an `eolas` command for browsing, fetching, and
|
|
76
|
+
scheduling — useful for shell scripts, cron jobs, and AI-agent workflows. Output
|
|
77
|
+
auto-detects piping: rich tables in a terminal, newline-delimited JSON when
|
|
78
|
+
stdout is piped.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# one-time setup
|
|
82
|
+
eolas auth set-key
|
|
83
|
+
eolas health
|
|
84
|
+
|
|
85
|
+
# discover
|
|
86
|
+
eolas datasets list --source "Stats NZ"
|
|
87
|
+
eolas datasets list --search cpi --json | jq '.[].name'
|
|
88
|
+
eolas datasets info nz_cpi
|
|
89
|
+
eolas datasets preview nz_cpi --limit 5
|
|
90
|
+
|
|
91
|
+
# fetch (verb matches the Python lib's client.get())
|
|
92
|
+
eolas get nz_cpi --format csv > cpi.csv
|
|
93
|
+
eolas get nz_cpi --start 2020-01-01 --format json | jq '.[].value'
|
|
94
|
+
eolas get sa2_2023 --format parquet --out sa2.parquet
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Scheduling
|
|
98
|
+
|
|
99
|
+
Set up recurring fetches without touching crontab/Task Scheduler syntax. Works
|
|
100
|
+
on Linux, macOS (cron), and Windows (Task Scheduler).
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
eolas schedule add nz_cpi --daily --out ~/data/cpi.csv
|
|
104
|
+
eolas schedule add nz_gdp --weekly --out ~/data/gdp.csv
|
|
105
|
+
eolas schedule add nzd_usd --cron "0 */6 * * *" --out ~/data/fx.csv # POSIX only
|
|
106
|
+
|
|
107
|
+
eolas schedule list
|
|
108
|
+
eolas schedule remove nz_cpi
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Daily is the default. Pre-flight check refuses to install a schedule unless
|
|
112
|
+
your API key is configured (otherwise the job would fail silently forever).
|
|
113
|
+
|
|
114
|
+
### Integrations (Enterprise plan)
|
|
115
|
+
|
|
116
|
+
Generate ready-to-run connector configs for popular data-pipeline tools — eolas
|
|
117
|
+
becomes a one-command source for Meltano, Fivetran, or Azure Data Factory.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
eolas integrate meltano --datasets nz_cpi,nz_gdp --output ./my-pipeline/
|
|
121
|
+
eolas integrate fivetran --datasets nz_cpi
|
|
122
|
+
eolas integrate azure-data-factory --datasets nz_cpi,nz_gdp
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The generated directory has everything needed to plug into your destination
|
|
126
|
+
warehouse: `meltano.yml`, `fivetran.yml`, or ADF JSON resources, plus a `README.md`
|
|
127
|
+
walking through the rest of the setup. Non-Enterprise users see a clear
|
|
128
|
+
upgrade pointer; the gating lives server-side so the capability is bypass-proof.
|
|
129
|
+
|
|
130
|
+
### Exit codes
|
|
131
|
+
|
|
132
|
+
Distinct exit codes per error class, for shell scripts and agents:
|
|
133
|
+
|
|
134
|
+
| Code | Meaning |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `0` | Success |
|
|
137
|
+
| `1` | Generic error |
|
|
138
|
+
| `2` | Auth (`AuthenticationError`, including Enterprise-gate 403) |
|
|
139
|
+
| `3` | Rate limit hit |
|
|
140
|
+
| `4` | Dataset / resource not found |
|
|
141
|
+
| `5` | Other API error |
|
|
142
|
+
| `64` | Bad usage (mirrors `sysexits.h`) |
|
|
143
|
+
|
|
144
|
+
## Geospatial
|
|
145
|
+
|
|
146
|
+
Datasets with a `geometry_wkt` column auto-convert to `geopandas.GeoDataFrame` if `geopandas` is installed:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
pip install eolas-data[geo]
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
gdf = client.get("nz_addresses") # GeoDataFrame
|
|
154
|
+
df = client.get("nz_addresses", as_geo=False) # plain DataFrame, WKT preserved
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Polars
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
pip install eolas-data[polars]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
df = client.get("nz_cpi", engine="polars")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Plotting
|
|
168
|
+
|
|
169
|
+
`Dataset` is a `pandas.DataFrame` subclass — use matplotlib / seaborn / plotly
|
|
170
|
+
directly. No bundled plot helper, because there's no universal "right" plot for
|
|
171
|
+
a tidy dataset (single-series time series vs. wide multi-measure vs. WKT
|
|
172
|
+
geometry all need different code).
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import matplotlib.pyplot as plt
|
|
176
|
+
|
|
177
|
+
df = client.statsnz("nz_cpi")
|
|
178
|
+
df.plot(x="date", y="value")
|
|
179
|
+
plt.show()
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Type stubs
|
|
183
|
+
|
|
184
|
+
Dataset names are exposed as a `Literal` so IDEs autocomplete the catalog:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from eolas_data import Client
|
|
188
|
+
|
|
189
|
+
client = Client()
|
|
190
|
+
client.get("nz_") # autocomplete shows nz_cpi, nz_gdp_production_annual, ...
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
The list is regenerated from the live API at release time. Passing a name not in the snapshot still works at runtime — the type hint just won't autocomplete it. Catalog snapshot date is exposed as `eolas_data._dataset_names.CATALOG_SNAPSHOT_DATE`.
|
|
194
|
+
|
|
195
|
+
## Releasing
|
|
196
|
+
|
|
197
|
+
See [`docs/clients.md`](https://github.com/phildonovan/eolas/blob/master/docs/clients.md) in the eolas data repo for the tagged-release flow and PyPI token rotation.
|
|
198
|
+
|
|
199
|
+
Before each release: `python -m eolas_data._regen_names` to refresh the dataset name stubs from the live API, commit the change, then tag and push.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
eolas_data/__init__.py,sha256=xiONh2SG6TBa2IylGmqGNfTrdPUGYL9GaIuXD09v-oA,399
|
|
2
|
+
eolas_data/_dataset_names.py,sha256=YaVe6QaeRYh-2xfwQ1mPr2ZsSQRofIfr8rPMsetSiEk,95541
|
|
3
|
+
eolas_data/_regen_names.py,sha256=zpU3_C19DT8LZC09VZuVK_-D9tVGtPa5x8DLEO5flwM,1898
|
|
4
|
+
eolas_data/cli.py,sha256=Vz0R4hBGJlS59XajZi81LT6sxxpOUcAZP4grWpWwETg,27386
|
|
5
|
+
eolas_data/client.py,sha256=KlfEXrbC5cFu8QDgiAauTnKuMYCghBHCcYviHoA36jw,27928
|
|
6
|
+
eolas_data/dataset.py,sha256=XLoNCdA4AHaP-XjYKMQ6LRnAOh4sYyVuveUO5HjRbAw,1661
|
|
7
|
+
eolas_data/exceptions.py,sha256=AJXOgymkMFqQP4Q_zdfkkGs2lrOCUpB_1xbWRquGnec,404
|
|
8
|
+
eolas_data/schedule.py,sha256=3npQ9fWg7pU_Ll1wmsA-z3zLdMCXtQTcW-yF68-k__Q,10106
|
|
9
|
+
eolas_data-1.0.0.dist-info/METADATA,sha256=jAfIWR0xTYW4WS_jNTH9XeYpLV9kCei8oVU9J3irgLU,6571
|
|
10
|
+
eolas_data-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
eolas_data-1.0.0.dist-info/entry_points.txt,sha256=RKhA3k4kfBLH5S7Rvep2j6B_eKiEHlY70MXxJfvVNVo,45
|
|
12
|
+
eolas_data-1.0.0.dist-info/RECORD,,
|