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/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)