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/client.py
ADDED
|
@@ -0,0 +1,680 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional, Union
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .dataset import Dataset
|
|
10
|
+
from .exceptions import APIError, AuthenticationError, NotFoundError, RateLimitError
|
|
11
|
+
|
|
12
|
+
# Imported separately so the names module is also re-exportable for users who
|
|
13
|
+
# want IDE autocomplete on dataset names without instantiating a Client.
|
|
14
|
+
from ._dataset_names import DatasetName # noqa: F401 (public re-export)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
BASE_URL = "https://api.eolas.fyi"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _to_geodataframe(df: "pd.DataFrame", force: bool = False):
|
|
21
|
+
"""Convert a DataFrame with a ``geometry_wkt`` column to a GeoDataFrame (CRS WGS84).
|
|
22
|
+
|
|
23
|
+
Returns the GeoDataFrame on success, or ``None`` when geopandas isn't installed
|
|
24
|
+
(and ``force`` is False) so the caller can fall back to the plain DataFrame.
|
|
25
|
+
Raises ImportError when ``force=True`` but geopandas is missing.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import geopandas as gpd
|
|
29
|
+
from shapely import wkt as _wkt
|
|
30
|
+
except ImportError:
|
|
31
|
+
if force:
|
|
32
|
+
raise ImportError(
|
|
33
|
+
"geopandas + shapely are required to return geospatial datasets "
|
|
34
|
+
"as GeoDataFrames. Install with: pip install eolas-data[geo]"
|
|
35
|
+
)
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
geom = df["geometry_wkt"].apply(lambda s: _wkt.loads(s) if isinstance(s, str) and s else None)
|
|
39
|
+
gdf = gpd.GeoDataFrame(df.drop(columns=["geometry_wkt"]), geometry=geom, crs="EPSG:4326")
|
|
40
|
+
for attr in ("eolas_name", "eolas_source"):
|
|
41
|
+
if hasattr(df, attr):
|
|
42
|
+
try:
|
|
43
|
+
setattr(gdf, attr, getattr(df, attr))
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
return gdf
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Client:
|
|
50
|
+
"""Client for the eolas.fyi statistical data API.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
api_key: Your API key. Falls back to the ``EOLAS_API_KEY`` env var.
|
|
54
|
+
base_url: Override the API base URL (useful for testing).
|
|
55
|
+
cache: Cache responses in memory for the lifetime of the client.
|
|
56
|
+
Useful in notebooks to avoid re-fetching on re-runs.
|
|
57
|
+
|
|
58
|
+
Examples::
|
|
59
|
+
|
|
60
|
+
from eolas_data import Client
|
|
61
|
+
client = Client("your_api_key")
|
|
62
|
+
|
|
63
|
+
# Source-specific helpers
|
|
64
|
+
df = client.statsnz("nz_cpi", start="2020-01-01")
|
|
65
|
+
df = client.oecd("nz_gdp")
|
|
66
|
+
|
|
67
|
+
# Generic
|
|
68
|
+
df = client.get("nz_cpi")
|
|
69
|
+
|
|
70
|
+
# Discovery
|
|
71
|
+
all_datasets = client.list()
|
|
72
|
+
nz_datasets = client.list("Stats NZ")
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
api_key: Optional[str] = None,
|
|
78
|
+
base_url: str = BASE_URL,
|
|
79
|
+
cache: bool = False,
|
|
80
|
+
):
|
|
81
|
+
self._key = api_key or os.getenv("EOLAS_API_KEY") or ""
|
|
82
|
+
self._base = base_url.rstrip("/")
|
|
83
|
+
self._cache: dict | None = {} if cache else None
|
|
84
|
+
self._session = requests.Session()
|
|
85
|
+
self._session.headers.update({"X-API-Key": self._key})
|
|
86
|
+
|
|
87
|
+
def __repr__(self) -> str:
|
|
88
|
+
masked = self._key[:8] + "..." if len(self._key) > 8 else self._key
|
|
89
|
+
cache = " cache=on" if self._cache is not None else ""
|
|
90
|
+
return f"<eolas_data.Client key={masked!r}{cache}>"
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------
|
|
93
|
+
# Discovery
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def list(self, source: Optional[str] = None) -> list[dict]:
|
|
97
|
+
"""Return metadata for all available datasets.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
source: Optional filter, e.g. ``"Stats NZ"``, ``"OECD"``.
|
|
101
|
+
"""
|
|
102
|
+
data = self._get("/v1/datasets")
|
|
103
|
+
items = data.get("datasets", data) if isinstance(data, dict) else data
|
|
104
|
+
if source:
|
|
105
|
+
items = [s for s in items if s.get("source") == source]
|
|
106
|
+
return items
|
|
107
|
+
|
|
108
|
+
def info(self, name: Union[str, "DatasetName"]) -> dict:
|
|
109
|
+
"""Return metadata for a single dataset."""
|
|
110
|
+
return self._get(f"/v1/datasets/{name}")
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Integrations (Enterprise plan only)
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def integration(self, platform: str, datasets: list[str]) -> dict[str, str]:
|
|
117
|
+
"""Generate connector config files for a third-party data-pipeline tool.
|
|
118
|
+
|
|
119
|
+
Enterprise plan only. Other plans receive an
|
|
120
|
+
:class:`AuthenticationError` with the upgrade message in the detail.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
platform: One of ``"meltano"``, ``"fivetran"``, ``"azure-data-factory"``.
|
|
124
|
+
datasets: Dataset names to include in the generated config.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
``{filename: file_contents}`` ready to write to disk.
|
|
128
|
+
|
|
129
|
+
Examples::
|
|
130
|
+
|
|
131
|
+
files = client.integration("meltano", ["nz_cpi", "nz_gdp"])
|
|
132
|
+
for filename, content in files.items():
|
|
133
|
+
Path("./tap-eolas") / filename).write_text(content)
|
|
134
|
+
"""
|
|
135
|
+
if not datasets:
|
|
136
|
+
raise ValueError("datasets cannot be empty")
|
|
137
|
+
resp = self._get(
|
|
138
|
+
f"/v1/integrations/{platform}",
|
|
139
|
+
params={"datasets": ",".join(datasets)},
|
|
140
|
+
)
|
|
141
|
+
return resp.get("files", {})
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------------
|
|
144
|
+
# Source-specific helpers
|
|
145
|
+
# ------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def statsnz(self, name, **kwargs) -> Dataset:
|
|
148
|
+
"""Fetch a Stats NZ dataset."""
|
|
149
|
+
return self._get_source(name, "Stats NZ", **kwargs)
|
|
150
|
+
|
|
151
|
+
def oecd(self, name, **kwargs) -> Dataset:
|
|
152
|
+
"""Fetch an OECD dataset."""
|
|
153
|
+
return self._get_source(name, "OECD", **kwargs)
|
|
154
|
+
|
|
155
|
+
def rbnz(self, name, **kwargs) -> Dataset:
|
|
156
|
+
"""Fetch an RBNZ dataset."""
|
|
157
|
+
return self._get_source(name, "RBNZ", **kwargs)
|
|
158
|
+
|
|
159
|
+
def treasury(self, name, **kwargs) -> Dataset:
|
|
160
|
+
"""Fetch an NZ Treasury dataset."""
|
|
161
|
+
return self._get_source(name, "NZ Treasury", **kwargs)
|
|
162
|
+
|
|
163
|
+
def linz(self, name, **kwargs) -> Dataset:
|
|
164
|
+
"""Fetch a LINZ dataset."""
|
|
165
|
+
return self._get_source(name, "LINZ", **kwargs)
|
|
166
|
+
|
|
167
|
+
def statsnz_geo(self, name, **kwargs) -> Dataset:
|
|
168
|
+
"""Fetch a Stats NZ geospatial dataset (boundaries, census meshblocks, etc.).
|
|
169
|
+
|
|
170
|
+
Kept as a convenience helper for discoverability — the server returns
|
|
171
|
+
``source = "Stats NZ"`` for both SDMX time series and Datafinder
|
|
172
|
+
geospatial datasets, so the metadata on the returned Dataset reads
|
|
173
|
+
``"Stats NZ"`` (not ``"Stats NZ Geospatial"``).
|
|
174
|
+
"""
|
|
175
|
+
return self._get_source(name, "Stats NZ", **kwargs)
|
|
176
|
+
|
|
177
|
+
def mbie(self, name, **kwargs) -> Dataset:
|
|
178
|
+
"""Fetch an MBIE dataset."""
|
|
179
|
+
return self._get_source(name, "MBIE", **kwargs)
|
|
180
|
+
|
|
181
|
+
def nzta(self, name, **kwargs) -> Dataset:
|
|
182
|
+
"""Fetch a Waka Kotahi (NZTA) dataset."""
|
|
183
|
+
return self._get_source(name, "Waka Kotahi", **kwargs)
|
|
184
|
+
|
|
185
|
+
def msd(self, name, **kwargs) -> Dataset:
|
|
186
|
+
"""Fetch an MSD dataset."""
|
|
187
|
+
return self._get_source(name, "MSD", **kwargs)
|
|
188
|
+
|
|
189
|
+
def police(self, name, **kwargs) -> Dataset:
|
|
190
|
+
"""Fetch an NZ Police / MoJ dataset."""
|
|
191
|
+
return self._get_source(name, "NZ Police / MoJ", **kwargs)
|
|
192
|
+
|
|
193
|
+
def acc(self, name, **kwargs) -> Dataset:
|
|
194
|
+
"""Fetch an ACC dataset."""
|
|
195
|
+
return self._get_source(name, "ACC", **kwargs)
|
|
196
|
+
|
|
197
|
+
def edcounts(self, name, **kwargs) -> Dataset:
|
|
198
|
+
"""Fetch an Education Counts dataset."""
|
|
199
|
+
return self._get_source(name, "Education Counts", **kwargs)
|
|
200
|
+
|
|
201
|
+
def worksafe(self, name, **kwargs) -> Dataset:
|
|
202
|
+
"""Fetch a WorkSafe NZ dataset."""
|
|
203
|
+
return self._get_source(name, "WorkSafe NZ", **kwargs)
|
|
204
|
+
|
|
205
|
+
def immigration(self, name, **kwargs) -> Dataset:
|
|
206
|
+
"""Fetch an Immigration NZ dataset."""
|
|
207
|
+
return self._get_source(name, "Immigration NZ", **kwargs)
|
|
208
|
+
|
|
209
|
+
def geonet(self, name, **kwargs) -> Dataset:
|
|
210
|
+
"""Fetch a GeoNet dataset (NZ earthquakes, volcanic alert levels, strong-motion sensors).
|
|
211
|
+
|
|
212
|
+
Examples::
|
|
213
|
+
|
|
214
|
+
client.geonet("geonet_quakes_recent") # rolling ~100 recent MMI>=3 quakes
|
|
215
|
+
client.geonet("geonet_volcanic_alert_levels") # 12 monitored NZ volcanoes
|
|
216
|
+
client.geonet("geonet_strong_motion_sensors") # 25 strong-motion stations
|
|
217
|
+
|
|
218
|
+
Notes:
|
|
219
|
+
Refreshed every 6 hours from api.geonet.org.nz. Earthquake catalogue is a
|
|
220
|
+
rolling window of recent events, not a historical archive. CC-BY 3.0 NZ
|
|
221
|
+
(Earth Sciences New Zealand, formerly GNS Science).
|
|
222
|
+
"""
|
|
223
|
+
return self._get_source(name, "GeoNet", **kwargs)
|
|
224
|
+
|
|
225
|
+
def lris(self, name, **kwargs) -> Dataset:
|
|
226
|
+
"""Fetch a Manaaki Whenua LRIS dataset (land cover, soil, protected areas).
|
|
227
|
+
|
|
228
|
+
Examples::
|
|
229
|
+
|
|
230
|
+
client.lris("lcdb_v6_mainland") # current NZ land cover (~543k polygons)
|
|
231
|
+
client.lris("nzlum_v03") # NZ Land Use Management v0.3
|
|
232
|
+
client.lris("pan_nz_2025_draft") # protected areas (Draft, 2025)
|
|
233
|
+
|
|
234
|
+
Notes:
|
|
235
|
+
LCDB v3.0–v4.1 are deprecated vintages, retained for longitudinal
|
|
236
|
+
analysis. LCDB v5 is superseded by v6 but still served.
|
|
237
|
+
PAN-NZ 2025 was marked Draft at the time of ingestion (2026-05-12).
|
|
238
|
+
Source: https://lris.scinfo.org.nz
|
|
239
|
+
Licence: CC-BY 4.0 International (LCDB v5/v6, NZLUM, PBC, PAN-NZ);
|
|
240
|
+
CC-BY 3.0 NZ (LCDB v3/v4 vintages). Attribution: Manaaki Whenua.
|
|
241
|
+
"""
|
|
242
|
+
return self._get_source(name, "Manaaki Whenua / LRIS", **kwargs)
|
|
243
|
+
|
|
244
|
+
def doc(self, name, **kwargs) -> Dataset:
|
|
245
|
+
"""Fetch a DOC (Department of Conservation) dataset.
|
|
246
|
+
|
|
247
|
+
Examples::
|
|
248
|
+
|
|
249
|
+
client.doc("doc_public_conservation_land") # ~11k polygons of NZ public conservation land
|
|
250
|
+
client.doc("doc_huts") # 1,429 DOC huts (Point geometry)
|
|
251
|
+
client.doc("doc_tracks") # 3,248 DOC tracks (Polyline)
|
|
252
|
+
|
|
253
|
+
Notes:
|
|
254
|
+
Refreshed weekly from DOC's ArcGIS hub. Operational alert streams
|
|
255
|
+
(track closures, hazard notices) are wired but currently blocked on
|
|
256
|
+
an API key issue; they will appear automatically once resolved.
|
|
257
|
+
CC-BY 4.0 International (Crown / Department of Conservation).
|
|
258
|
+
"""
|
|
259
|
+
return self._get_source(name, "DOC", **kwargs)
|
|
260
|
+
|
|
261
|
+
def akl_council(self, name, **kwargs) -> Dataset:
|
|
262
|
+
"""Fetch an Auckland Council dataset (overlays, heritage, hazards, zoning).
|
|
263
|
+
|
|
264
|
+
Examples::
|
|
265
|
+
|
|
266
|
+
client.akl_council("akc_notable_trees_overlay")
|
|
267
|
+
client.akl_council("akc_significant_ecological_areas_overlay")
|
|
268
|
+
client.akl_council("akc_historic_heritage_overlay_place")
|
|
269
|
+
|
|
270
|
+
Notes:
|
|
271
|
+
Open data from the Auckland Council ArcGIS hub. Covers district
|
|
272
|
+
plan overlays, heritage areas, ecological areas, stormwater
|
|
273
|
+
management zones, and more. CC-BY 4.0 (Auckland Council).
|
|
274
|
+
Source: https://data-aucklandcouncil.opendata.arcgis.com
|
|
275
|
+
"""
|
|
276
|
+
return self._get_source(name, "Auckland Council", **kwargs)
|
|
277
|
+
|
|
278
|
+
def akl_transport(self, name, **kwargs) -> Dataset:
|
|
279
|
+
"""Fetch an Auckland Transport dataset (roads, public transport, cycling).
|
|
280
|
+
|
|
281
|
+
Examples::
|
|
282
|
+
|
|
283
|
+
client.akl_transport("akt_bus_stop")
|
|
284
|
+
client.akl_transport("akt_bus_route")
|
|
285
|
+
client.akl_transport("akt_cycle_facility_network")
|
|
286
|
+
|
|
287
|
+
Notes:
|
|
288
|
+
Open data from Auckland Transport (AT). Covers bus stops,
|
|
289
|
+
bus routes, bridges, cycle infrastructure, and more.
|
|
290
|
+
CC-BY 4.0 (Auckland Transport).
|
|
291
|
+
Source: https://data-atgis.opendata.arcgis.com
|
|
292
|
+
"""
|
|
293
|
+
return self._get_source(name, "Auckland Transport", **kwargs)
|
|
294
|
+
|
|
295
|
+
def bay_of_plenty(self, name, **kwargs) -> Dataset:
|
|
296
|
+
"""Fetch a Bay of Plenty Councils dataset (hazards, resource consents, planning).
|
|
297
|
+
|
|
298
|
+
Examples::
|
|
299
|
+
|
|
300
|
+
client.bay_of_plenty("boprc_historic_flood_extents")
|
|
301
|
+
client.bay_of_plenty("boprc_liquefaction_level_b")
|
|
302
|
+
client.bay_of_plenty("boprc_rcep_ascv")
|
|
303
|
+
|
|
304
|
+
Notes:
|
|
305
|
+
Open data from Bay of Plenty Regional Council and its territorial
|
|
306
|
+
authorities. Covers flood extents, liquefaction, coastal hazards,
|
|
307
|
+
resource consents, and planning layers. CC-BY 4.0.
|
|
308
|
+
Source: https://www.boprc.govt.nz
|
|
309
|
+
"""
|
|
310
|
+
return self._get_source(name, "Bay of Plenty Councils", **kwargs)
|
|
311
|
+
|
|
312
|
+
def charities(self, name, **kwargs) -> Dataset:
|
|
313
|
+
"""Fetch a Charities Services dataset (registered NZ charities).
|
|
314
|
+
|
|
315
|
+
Examples::
|
|
316
|
+
|
|
317
|
+
client.charities("charities_organisations")
|
|
318
|
+
client.charities("charities_annual_returns")
|
|
319
|
+
client.charities("charities_activities")
|
|
320
|
+
|
|
321
|
+
Notes:
|
|
322
|
+
Data from Charities Services (a business unit of the Department
|
|
323
|
+
of Internal Affairs). Covers registered charities, officers,
|
|
324
|
+
beneficiary groups, and annual financial returns.
|
|
325
|
+
Open Government Licence v3.0.
|
|
326
|
+
Source: https://www.charities.govt.nz
|
|
327
|
+
"""
|
|
328
|
+
return self._get_source(name, "Charities Services", **kwargs)
|
|
329
|
+
|
|
330
|
+
def colab_waikato(self, name, **kwargs) -> Dataset:
|
|
331
|
+
"""Fetch a Co-Lab Waikato dataset (planning, hazards, heritage across Waikato councils).
|
|
332
|
+
|
|
333
|
+
Examples::
|
|
334
|
+
|
|
335
|
+
client.colab_waikato("wmkdc_buildings")
|
|
336
|
+
client.colab_waikato("tcdc_dp_coastal_environment")
|
|
337
|
+
client.colab_waikato("wbopdc_coastal_erosion")
|
|
338
|
+
|
|
339
|
+
Notes:
|
|
340
|
+
Data aggregated via the Co-Lab Waikato open data hub. Covers
|
|
341
|
+
district plan zones, coastal hazards, heritage, and building
|
|
342
|
+
footprints across Waikato-region territorial authorities.
|
|
343
|
+
CC-BY 4.0 (respective councils).
|
|
344
|
+
Source: https://data-waikatolass.opendata.arcgis.com
|
|
345
|
+
"""
|
|
346
|
+
return self._get_source(name, "Co-Lab Waikato", **kwargs)
|
|
347
|
+
|
|
348
|
+
def ecan_canterbury(self, name, **kwargs) -> Dataset:
|
|
349
|
+
"""Fetch an ECan / Canterbury dataset (environment, hazards, resource consents).
|
|
350
|
+
|
|
351
|
+
Examples::
|
|
352
|
+
|
|
353
|
+
client.ecan_canterbury("ecan_liquefaction_susceptibility_final")
|
|
354
|
+
client.ecan_canterbury("ecan_tsunami_evacuation_zones")
|
|
355
|
+
client.ecan_canterbury("ecan_resource_consents_active_all")
|
|
356
|
+
|
|
357
|
+
Notes:
|
|
358
|
+
Open data from Environment Canterbury (ECan) and Canterbury-region
|
|
359
|
+
councils. Covers liquefaction, earthquake faults, tsunami zones,
|
|
360
|
+
water allocation, resource consents, and planning layers.
|
|
361
|
+
CC-BY 4.0 (Environment Canterbury / respective councils).
|
|
362
|
+
Source: https://opendata.canterburymaps.govt.nz
|
|
363
|
+
"""
|
|
364
|
+
return self._get_source(name, "ECan / Canterbury", **kwargs)
|
|
365
|
+
|
|
366
|
+
def hawkes_bay(self, name, **kwargs) -> Dataset:
|
|
367
|
+
"""Fetch a Hawke's Bay Councils dataset (hazards, planning, coastal management).
|
|
368
|
+
|
|
369
|
+
Examples::
|
|
370
|
+
|
|
371
|
+
client.hawkes_bay("hbrc_coastal_erosion_likely_66")
|
|
372
|
+
client.hawkes_bay("hbrc_coastal_erosion_possible_33")
|
|
373
|
+
client.hawkes_bay("hbrc_chb_hdc_wdc_liquefaction_severity")
|
|
374
|
+
|
|
375
|
+
Notes:
|
|
376
|
+
Open data from Hawke's Bay Regional Council and its territorial
|
|
377
|
+
authorities. Covers coastal erosion, liquefaction, flood hazards,
|
|
378
|
+
and district planning layers. CC-BY 4.0.
|
|
379
|
+
Source: https://www.hbrc.govt.nz
|
|
380
|
+
"""
|
|
381
|
+
return self._get_source(name, "Hawke's Bay Councils", **kwargs)
|
|
382
|
+
|
|
383
|
+
def manawatu_whanganui(self, name, **kwargs) -> Dataset:
|
|
384
|
+
"""Fetch a Manawatu-Whanganui Councils dataset (airsheds, coastal, freshwater).
|
|
385
|
+
|
|
386
|
+
Examples::
|
|
387
|
+
|
|
388
|
+
client.manawatu_whanganui("horizons_coastal_marine_area")
|
|
389
|
+
client.manawatu_whanganui("horizons_airshed_taihape")
|
|
390
|
+
client.manawatu_whanganui("horizons_airshed_taumarunui")
|
|
391
|
+
|
|
392
|
+
Notes:
|
|
393
|
+
Open data from Horizons Regional Council (Manawatu-Whanganui) and
|
|
394
|
+
its territorial authorities. Covers airsheds, coastal marine areas,
|
|
395
|
+
freshwater, and planning layers. CC-BY 4.0.
|
|
396
|
+
Source: https://www.horizons.govt.nz
|
|
397
|
+
"""
|
|
398
|
+
return self._get_source(name, "Manawatu-Whanganui Councils", **kwargs)
|
|
399
|
+
|
|
400
|
+
def napier_whanganui(self, name, **kwargs) -> Dataset:
|
|
401
|
+
"""Fetch a Napier or Whanganui city dataset (district plan, heritage, infrastructure).
|
|
402
|
+
|
|
403
|
+
Examples::
|
|
404
|
+
|
|
405
|
+
client.napier_whanganui("napier_heritage_buildings")
|
|
406
|
+
client.napier_whanganui("napier_address_points")
|
|
407
|
+
client.napier_whanganui("napier_parcels")
|
|
408
|
+
|
|
409
|
+
Notes:
|
|
410
|
+
Open data from Napier City Council and Whanganui District Council.
|
|
411
|
+
Covers district plan precincts, heritage buildings and areas,
|
|
412
|
+
address points, road centrelines, and parcels. CC-BY 4.0.
|
|
413
|
+
Source: https://www.napier.govt.nz / https://www.whanganui.govt.nz
|
|
414
|
+
"""
|
|
415
|
+
return self._get_source(name, "Napier + Whanganui", **kwargs)
|
|
416
|
+
|
|
417
|
+
def northland(self, name, **kwargs) -> Dataset:
|
|
418
|
+
"""Fetch a Northland Councils dataset (district plans, designations, heritage).
|
|
419
|
+
|
|
420
|
+
Examples::
|
|
421
|
+
|
|
422
|
+
client.northland("fndc_district_plan_zones")
|
|
423
|
+
client.northland("fndc_heritage_areas")
|
|
424
|
+
client.northland("fndc_designations")
|
|
425
|
+
|
|
426
|
+
Notes:
|
|
427
|
+
Open data from Northland Regional Council and its territorial
|
|
428
|
+
authorities (Far North, Whangarei, Kaipara). Covers district plan
|
|
429
|
+
zones, designations, heritage, and environmental layers. CC-BY 4.0.
|
|
430
|
+
Source: https://www.nrc.govt.nz
|
|
431
|
+
"""
|
|
432
|
+
return self._get_source(name, "Northland Councils", **kwargs)
|
|
433
|
+
|
|
434
|
+
def otago(self, name, **kwargs) -> Dataset:
|
|
435
|
+
"""Fetch an Otago Councils dataset (land use, water, planning, hazards).
|
|
436
|
+
|
|
437
|
+
Examples::
|
|
438
|
+
|
|
439
|
+
client.otago("orc_otago_irrigated_areas")
|
|
440
|
+
client.otago("orc_otago_land_use_2024")
|
|
441
|
+
client.otago("orc_floodbanks")
|
|
442
|
+
|
|
443
|
+
Notes:
|
|
444
|
+
Open data from Otago Regional Council and its territorial
|
|
445
|
+
authorities (Dunedin, Queenstown-Lakes, Central Otago, Clutha,
|
|
446
|
+
Waitaki). Covers land use, floodbanks, groundwater protection,
|
|
447
|
+
and planning layers. CC-BY 4.0.
|
|
448
|
+
Source: https://www.orc.govt.nz
|
|
449
|
+
"""
|
|
450
|
+
return self._get_source(name, "Otago Councils", **kwargs)
|
|
451
|
+
|
|
452
|
+
def southland(self, name, **kwargs) -> Dataset:
|
|
453
|
+
"""Fetch a Southland Councils dataset (district plans, coastal, natural hazards).
|
|
454
|
+
|
|
455
|
+
Examples::
|
|
456
|
+
|
|
457
|
+
client.southland("sdc_southland_dp_zones")
|
|
458
|
+
client.southland("sdc_southland_dp_heritage_items")
|
|
459
|
+
client.southland("es_southland_land_use_2025")
|
|
460
|
+
|
|
461
|
+
Notes:
|
|
462
|
+
Open data from Environment Southland and its territorial
|
|
463
|
+
authorities (Southland District, Gore, Invercargill). Covers
|
|
464
|
+
district plan zones, coastal hazards, heritage, and land use.
|
|
465
|
+
CC-BY 4.0.
|
|
466
|
+
Source: https://www.es.govt.nz
|
|
467
|
+
"""
|
|
468
|
+
return self._get_source(name, "Southland Councils", **kwargs)
|
|
469
|
+
|
|
470
|
+
def taranaki(self, name, **kwargs) -> Dataset:
|
|
471
|
+
"""Fetch a Taranaki Councils dataset (coastal, biodiversity, district plans).
|
|
472
|
+
|
|
473
|
+
Examples::
|
|
474
|
+
|
|
475
|
+
client.taranaki("trc_biodiversity_coastal_mgmt_areas")
|
|
476
|
+
client.taranaki("npdc_dp_operative_coastal_flooding")
|
|
477
|
+
client.taranaki("npdc_dp_operative_archaeological")
|
|
478
|
+
|
|
479
|
+
Notes:
|
|
480
|
+
Open data from Taranaki Regional Council and its territorial
|
|
481
|
+
authorities (New Plymouth, Stratford, South Taranaki). Covers
|
|
482
|
+
biodiversity, coastal management, and district planning layers.
|
|
483
|
+
CC-BY 4.0.
|
|
484
|
+
Source: https://www.trc.govt.nz
|
|
485
|
+
"""
|
|
486
|
+
return self._get_source(name, "Taranaki Councils", **kwargs)
|
|
487
|
+
|
|
488
|
+
def top_of_south(self, name, **kwargs) -> Dataset:
|
|
489
|
+
"""Fetch a Gisborne / Top of South Councils dataset (coastal, planning, heritage).
|
|
490
|
+
|
|
491
|
+
Examples::
|
|
492
|
+
|
|
493
|
+
client.top_of_south("gdc_coastal_environment")
|
|
494
|
+
client.top_of_south("gdc_coastal_erosion")
|
|
495
|
+
client.top_of_south("gdc_coastal_flooding")
|
|
496
|
+
|
|
497
|
+
Notes:
|
|
498
|
+
Open data from Gisborne District Council, Marlborough District
|
|
499
|
+
Council, Nelson City Council, and Tasman District Council.
|
|
500
|
+
Covers coastal hazards, planning zones, and heritage layers.
|
|
501
|
+
CC-BY 4.0.
|
|
502
|
+
Source: https://www.gdc.govt.nz
|
|
503
|
+
"""
|
|
504
|
+
return self._get_source(name, "Gisborne / Top of South Councils", **kwargs)
|
|
505
|
+
|
|
506
|
+
def wellington(self, name, **kwargs) -> Dataset:
|
|
507
|
+
"""Fetch a Wellington Region Councils dataset (hazards, planning, infrastructure).
|
|
508
|
+
|
|
509
|
+
Examples::
|
|
510
|
+
|
|
511
|
+
client.wellington("wcc_district_plan_zones_2024")
|
|
512
|
+
client.wellington("wcc_flood_hazard_operative")
|
|
513
|
+
client.wellington("gwrc_flood_hazard_extents")
|
|
514
|
+
|
|
515
|
+
Notes:
|
|
516
|
+
Open data from Greater Wellington Regional Council and its
|
|
517
|
+
territorial authorities (Wellington, Hutt, Upper Hutt, Porirua,
|
|
518
|
+
Kapiti Coast). Covers flood and earthquake hazards, district plan
|
|
519
|
+
zones, and coastal inundation. CC-BY 4.0.
|
|
520
|
+
Source: https://www.gw.govt.nz
|
|
521
|
+
"""
|
|
522
|
+
return self._get_source(name, "Wellington Region Councils", **kwargs)
|
|
523
|
+
|
|
524
|
+
def west_coast(self, name, **kwargs) -> Dataset:
|
|
525
|
+
"""Fetch a West Coast (Te Tai o Poutini) dataset (faults, landslides, planning).
|
|
526
|
+
|
|
527
|
+
Examples::
|
|
528
|
+
|
|
529
|
+
client.west_coast("wcrc_active_faults")
|
|
530
|
+
client.west_coast("wcrc_alpine_fault_traces")
|
|
531
|
+
client.west_coast("wcrc_landslide_catalog")
|
|
532
|
+
|
|
533
|
+
Notes:
|
|
534
|
+
Open data from West Coast Regional Council (Te Tai o Poutini) and
|
|
535
|
+
its territorial authorities (Buller, Grey, Westland). Covers
|
|
536
|
+
active faults, the Alpine Fault, landslide catalogs, and
|
|
537
|
+
significant natural areas. CC-BY 4.0.
|
|
538
|
+
Source: https://www.ttpp.nz
|
|
539
|
+
"""
|
|
540
|
+
return self._get_source(name, "West Coast (Te Tai o Poutini)", **kwargs)
|
|
541
|
+
|
|
542
|
+
def _get_source(self, name, source: str, **kwargs) -> Dataset:
|
|
543
|
+
df = self.get(name, **kwargs)
|
|
544
|
+
df.eolas_source = source
|
|
545
|
+
return df
|
|
546
|
+
|
|
547
|
+
# ------------------------------------------------------------------
|
|
548
|
+
# Core data fetch
|
|
549
|
+
# ------------------------------------------------------------------
|
|
550
|
+
|
|
551
|
+
def get(
|
|
552
|
+
self,
|
|
553
|
+
name: Union[str, "DatasetName"],
|
|
554
|
+
start: Optional[str] = None,
|
|
555
|
+
end: Optional[str] = None,
|
|
556
|
+
format: str = "json",
|
|
557
|
+
engine: str = "pandas",
|
|
558
|
+
limit: Optional[int] = None,
|
|
559
|
+
as_geo: Optional[bool] = None,
|
|
560
|
+
) -> Dataset:
|
|
561
|
+
"""Fetch dataset rows as a pandas (or polars / geopandas) DataFrame.
|
|
562
|
+
|
|
563
|
+
Args:
|
|
564
|
+
name: Dataset identifier, e.g. ``"nz_cpi"``. Type-checked against
|
|
565
|
+
the ``DatasetName`` Literal at static-analysis time so
|
|
566
|
+
IDEs autocomplete the catalog.
|
|
567
|
+
start: ISO date lower bound, e.g. ``"2020-01-01"``.
|
|
568
|
+
end: ISO date upper bound, e.g. ``"2024-12-31"``.
|
|
569
|
+
format: ``"json"`` (default) or ``"csv"``.
|
|
570
|
+
engine: ``"pandas"`` (default) or ``"polars"``.
|
|
571
|
+
limit: Max rows to return. Default ``None`` requests the full dataset
|
|
572
|
+
(server enforces a 50,000-row cap on Free/Starter plans; Pro is
|
|
573
|
+
unlimited). Pass an explicit integer to request fewer rows.
|
|
574
|
+
as_geo: Convert geospatial datasets to a ``GeoDataFrame``.
|
|
575
|
+
``None`` (default) auto-converts when the dataset has a
|
|
576
|
+
``geometry_wkt`` column AND ``geopandas`` is importable.
|
|
577
|
+
``True`` forces the conversion (raises if geopandas missing).
|
|
578
|
+
``False`` keeps the raw WKT string column.
|
|
579
|
+
Install with ``pip install eolas-data[geo]``.
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
A :class:`Dataset` (pandas DataFrame subclass), a polars DataFrame
|
|
583
|
+
when ``engine="polars"``, or a ``geopandas.GeoDataFrame`` when
|
|
584
|
+
geometry is present and conversion is enabled.
|
|
585
|
+
"""
|
|
586
|
+
params: dict = {}
|
|
587
|
+
if start:
|
|
588
|
+
params["start"] = start
|
|
589
|
+
if end:
|
|
590
|
+
params["end"] = end
|
|
591
|
+
# Server-side: limit=0 means "as much as the plan allows" (full dataset for Pro,
|
|
592
|
+
# 50K cap for Free/Starter). limit=None on the client maps to limit=0.
|
|
593
|
+
params["limit"] = 0 if limit is None else int(limit)
|
|
594
|
+
|
|
595
|
+
cache_key = f"{name}:{start}:{end}:{format}:{params['limit']}:{as_geo}"
|
|
596
|
+
if self._cache is not None and cache_key in self._cache:
|
|
597
|
+
return self._cache[cache_key]
|
|
598
|
+
|
|
599
|
+
if format == "csv":
|
|
600
|
+
from io import StringIO
|
|
601
|
+
resp = self._raw_get(f"/v1/datasets/{name}/data", params={"format": "csv", **params})
|
|
602
|
+
df = pd.read_csv(StringIO(resp.text))
|
|
603
|
+
else:
|
|
604
|
+
data = self._get(f"/v1/datasets/{name}/data", params=params)
|
|
605
|
+
records = data.get("data", data) if isinstance(data, dict) else data
|
|
606
|
+
df = pd.DataFrame(records)
|
|
607
|
+
if "date" in df.columns:
|
|
608
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
609
|
+
|
|
610
|
+
# API streams from Iceberg in file order, not chronological — sort here so
|
|
611
|
+
# callers can `df.plot(x="date", y="value")` without seeing zigzag lines.
|
|
612
|
+
if "date" in df.columns:
|
|
613
|
+
df = df.sort_values("date", kind="stable").reset_index(drop=True)
|
|
614
|
+
|
|
615
|
+
result = Dataset(df)
|
|
616
|
+
result.eolas_name = name
|
|
617
|
+
result.eolas_source = ""
|
|
618
|
+
|
|
619
|
+
if engine == "polars":
|
|
620
|
+
try:
|
|
621
|
+
import polars as pl
|
|
622
|
+
return pl.from_pandas(result)
|
|
623
|
+
except ImportError:
|
|
624
|
+
raise ImportError(
|
|
625
|
+
"polars is required for engine='polars'. "
|
|
626
|
+
"Install with: pip install eolas-data[polars]"
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
# Optional geopandas conversion. When as_geo=None we auto-convert if both
|
|
630
|
+
# (a) the dataset has a geometry_wkt column AND (b) geopandas is importable.
|
|
631
|
+
if as_geo is not False and "geometry_wkt" in result.columns:
|
|
632
|
+
converted = _to_geodataframe(result, force=as_geo is True)
|
|
633
|
+
if converted is not None:
|
|
634
|
+
result = converted
|
|
635
|
+
|
|
636
|
+
if self._cache is not None:
|
|
637
|
+
self._cache[cache_key] = result
|
|
638
|
+
|
|
639
|
+
return result
|
|
640
|
+
|
|
641
|
+
# ------------------------------------------------------------------
|
|
642
|
+
# HTTP helpers
|
|
643
|
+
# ------------------------------------------------------------------
|
|
644
|
+
|
|
645
|
+
def _get(self, path: str, params: Optional[dict] = None) -> dict:
|
|
646
|
+
return self._raw_get(path, params=params).json()
|
|
647
|
+
|
|
648
|
+
def _raw_get(self, path: str, params: Optional[dict] = None) -> requests.Response:
|
|
649
|
+
url = f"{self._base}{path}"
|
|
650
|
+
resp = self._session.get(url, params=params)
|
|
651
|
+
self._raise_for_status(resp)
|
|
652
|
+
return resp
|
|
653
|
+
|
|
654
|
+
@staticmethod
|
|
655
|
+
def _raise_for_status(resp: requests.Response) -> None:
|
|
656
|
+
if resp.status_code == 200:
|
|
657
|
+
return
|
|
658
|
+
if resp.status_code == 401:
|
|
659
|
+
raise AuthenticationError("Invalid or missing API key.")
|
|
660
|
+
if resp.status_code == 403:
|
|
661
|
+
try:
|
|
662
|
+
detail = resp.json().get("detail", "API key is inactive.")
|
|
663
|
+
except Exception:
|
|
664
|
+
detail = "API key is inactive."
|
|
665
|
+
raise AuthenticationError(detail)
|
|
666
|
+
if resp.status_code == 429:
|
|
667
|
+
raise RateLimitError(
|
|
668
|
+
"Monthly request limit reached. Upgrade for higher limits."
|
|
669
|
+
)
|
|
670
|
+
if resp.status_code == 404:
|
|
671
|
+
try:
|
|
672
|
+
detail = resp.json().get("detail", "Not found.")
|
|
673
|
+
except Exception:
|
|
674
|
+
detail = "Not found."
|
|
675
|
+
raise NotFoundError(detail)
|
|
676
|
+
try:
|
|
677
|
+
detail = resp.json().get("detail", resp.text)
|
|
678
|
+
except Exception:
|
|
679
|
+
detail = resp.text
|
|
680
|
+
raise APIError(resp.status_code, detail)
|