kardashev 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kardashev-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kardashev
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for the Kardashev Labs energy data API
|
|
5
|
+
Project-URL: Homepage, https://kardashevlabs.org
|
|
6
|
+
Project-URL: API, https://data.kardashevlabs.org/docs
|
|
7
|
+
Project-URL: Source, https://github.com/kardashev-lab/kardashev-py
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: httpx>=0.24
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pandas; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-httpx; extra == 'dev'
|
|
14
|
+
Provides-Extra: pandas
|
|
15
|
+
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Any, Optional, Union
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
_BASE = "https://data.kardashevlabs.org"
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
import pandas as pd
|
|
12
|
+
_PANDAS = True
|
|
13
|
+
except ImportError:
|
|
14
|
+
_PANDAS = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _to_df(data: list[dict]) -> Any:
|
|
18
|
+
if _PANDAS and data:
|
|
19
|
+
df = pd.DataFrame(data)
|
|
20
|
+
for col in df.columns:
|
|
21
|
+
if "ts" in col or col in ("start_time", "end_time", "report_date"):
|
|
22
|
+
try:
|
|
23
|
+
df[col] = pd.to_datetime(df[col], utc=True)
|
|
24
|
+
except Exception:
|
|
25
|
+
pass
|
|
26
|
+
return df
|
|
27
|
+
return data
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _fmt(d: Optional[date]) -> Optional[str]:
|
|
31
|
+
return d.isoformat() if d else None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Client:
|
|
35
|
+
"""Python client for the Kardashev Labs energy data API.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
base_url:
|
|
40
|
+
Override the API base URL (useful for local dev).
|
|
41
|
+
timeout:
|
|
42
|
+
HTTP timeout in seconds.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(
|
|
46
|
+
self,
|
|
47
|
+
base_url: str = _BASE,
|
|
48
|
+
timeout: float = 30.0,
|
|
49
|
+
) -> None:
|
|
50
|
+
self._http = httpx.Client(base_url=base_url.rstrip("/"), timeout=timeout)
|
|
51
|
+
|
|
52
|
+
def _get(self, path: str, **params: Any) -> list[dict]:
|
|
53
|
+
clean = {k: v for k, v in params.items() if v is not None}
|
|
54
|
+
r = self._http.get(path, params=clean)
|
|
55
|
+
r.raise_for_status()
|
|
56
|
+
return r.json()
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
# Fuel mix
|
|
60
|
+
# ------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
def fuel_mix(
|
|
63
|
+
self,
|
|
64
|
+
iso: str,
|
|
65
|
+
start: Optional[date] = None,
|
|
66
|
+
end: Optional[date] = None,
|
|
67
|
+
hours: int = 24,
|
|
68
|
+
limit: int = 2000,
|
|
69
|
+
) -> Any:
|
|
70
|
+
"""Real-time fuel mix (MW by fuel type) for an ISO."""
|
|
71
|
+
return _to_df(self._get(
|
|
72
|
+
"/fuel-mix",
|
|
73
|
+
iso=iso.upper(),
|
|
74
|
+
start=_fmt(start),
|
|
75
|
+
end=_fmt(end),
|
|
76
|
+
hours=hours,
|
|
77
|
+
limit=limit,
|
|
78
|
+
))
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------------
|
|
81
|
+
# Carbon intensity
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
def carbon(
|
|
85
|
+
self,
|
|
86
|
+
iso: str,
|
|
87
|
+
start: Optional[date] = None,
|
|
88
|
+
end: Optional[date] = None,
|
|
89
|
+
hours: int = 24,
|
|
90
|
+
limit: int = 2000,
|
|
91
|
+
) -> Any:
|
|
92
|
+
"""Hourly carbon intensity (lbs CO₂/MWh) for an ISO."""
|
|
93
|
+
return _to_df(self._get(
|
|
94
|
+
"/carbon",
|
|
95
|
+
iso=iso.upper(),
|
|
96
|
+
start=_fmt(start),
|
|
97
|
+
end=_fmt(end),
|
|
98
|
+
hours=hours,
|
|
99
|
+
limit=limit,
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
def carbon_latest(self, iso: Optional[str] = None) -> Any:
|
|
103
|
+
"""Latest carbon snapshot — one row per ISO."""
|
|
104
|
+
return _to_df(self._get("/carbon/latest", iso=iso.upper() if iso else None))
|
|
105
|
+
|
|
106
|
+
# ------------------------------------------------------------------
|
|
107
|
+
# LMP
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def lmp(
|
|
111
|
+
self,
|
|
112
|
+
iso: str,
|
|
113
|
+
node_id: Optional[str] = None,
|
|
114
|
+
market: str = "RT",
|
|
115
|
+
start: Optional[date] = None,
|
|
116
|
+
end: Optional[date] = None,
|
|
117
|
+
hours: int = 24,
|
|
118
|
+
limit: int = 2000,
|
|
119
|
+
) -> Any:
|
|
120
|
+
"""Locational marginal prices (LMP, energy, congestion, loss)."""
|
|
121
|
+
return _to_df(self._get(
|
|
122
|
+
"/lmp",
|
|
123
|
+
iso=iso.upper(),
|
|
124
|
+
node_id=node_id,
|
|
125
|
+
market=market.upper(),
|
|
126
|
+
start=_fmt(start),
|
|
127
|
+
end=_fmt(end),
|
|
128
|
+
hours=hours,
|
|
129
|
+
limit=limit,
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
def lmp_map(self, iso: str, market: str = "RT") -> Any:
|
|
133
|
+
"""Latest LMP for all nodes with lat/lng — for map rendering."""
|
|
134
|
+
return _to_df(self._get("/lmp/map", iso=iso.upper(), market=market.upper()))
|
|
135
|
+
|
|
136
|
+
def lmp_hubs(self, iso: Optional[str] = None) -> Any:
|
|
137
|
+
"""List all tracked LMP pricing nodes."""
|
|
138
|
+
return _to_df(self._get("/lmp/hubs", iso=iso.upper() if iso else None))
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
# Load
|
|
142
|
+
# ------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
def load(
|
|
145
|
+
self,
|
|
146
|
+
iso: Optional[str] = None,
|
|
147
|
+
start: Optional[date] = None,
|
|
148
|
+
end: Optional[date] = None,
|
|
149
|
+
hours: int = 24,
|
|
150
|
+
limit: int = 2000,
|
|
151
|
+
) -> Any:
|
|
152
|
+
"""Actual grid load (MW) by ISO."""
|
|
153
|
+
return _to_df(self._get(
|
|
154
|
+
"/load",
|
|
155
|
+
iso=iso.upper() if iso else None,
|
|
156
|
+
start=_fmt(start),
|
|
157
|
+
end=_fmt(end),
|
|
158
|
+
hours=hours,
|
|
159
|
+
limit=limit,
|
|
160
|
+
))
|
|
161
|
+
|
|
162
|
+
def load_forecast(
|
|
163
|
+
self,
|
|
164
|
+
iso: Optional[str] = None,
|
|
165
|
+
hours: int = 24,
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""Load forecast (MW) for the next N hours."""
|
|
168
|
+
return _to_df(self._get(
|
|
169
|
+
"/load/forecast",
|
|
170
|
+
iso=iso.upper() if iso else None,
|
|
171
|
+
hours=hours,
|
|
172
|
+
))
|
|
173
|
+
|
|
174
|
+
# ------------------------------------------------------------------
|
|
175
|
+
# Generation
|
|
176
|
+
# ------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def generation(
|
|
179
|
+
self,
|
|
180
|
+
iso: Optional[str] = None,
|
|
181
|
+
fuel_type: Optional[str] = None,
|
|
182
|
+
start: Optional[date] = None,
|
|
183
|
+
end: Optional[date] = None,
|
|
184
|
+
hours: int = 24,
|
|
185
|
+
limit: int = 2000,
|
|
186
|
+
) -> Any:
|
|
187
|
+
"""Generation by fuel type for an ISO."""
|
|
188
|
+
return _to_df(self._get(
|
|
189
|
+
"/generation",
|
|
190
|
+
iso=iso.upper() if iso else None,
|
|
191
|
+
fuel_type=fuel_type,
|
|
192
|
+
start=_fmt(start),
|
|
193
|
+
end=_fmt(end),
|
|
194
|
+
hours=hours,
|
|
195
|
+
limit=limit,
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
# ------------------------------------------------------------------
|
|
199
|
+
# Curtailment
|
|
200
|
+
# ------------------------------------------------------------------
|
|
201
|
+
|
|
202
|
+
def curtailment(
|
|
203
|
+
self,
|
|
204
|
+
iso: Optional[str] = None,
|
|
205
|
+
start: Optional[date] = None,
|
|
206
|
+
end: Optional[date] = None,
|
|
207
|
+
hours: int = 24,
|
|
208
|
+
limit: int = 2000,
|
|
209
|
+
) -> Any:
|
|
210
|
+
"""Renewable curtailment (MWh) by ISO."""
|
|
211
|
+
return _to_df(self._get(
|
|
212
|
+
"/curtailment",
|
|
213
|
+
iso=iso.upper() if iso else None,
|
|
214
|
+
start=_fmt(start),
|
|
215
|
+
end=_fmt(end),
|
|
216
|
+
hours=hours,
|
|
217
|
+
limit=limit,
|
|
218
|
+
))
|
|
219
|
+
|
|
220
|
+
# ------------------------------------------------------------------
|
|
221
|
+
# Interchange
|
|
222
|
+
# ------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
def interchange(
|
|
225
|
+
self,
|
|
226
|
+
ba: str,
|
|
227
|
+
start: Optional[date] = None,
|
|
228
|
+
end: Optional[date] = None,
|
|
229
|
+
hours: int = 24,
|
|
230
|
+
limit: int = 5000,
|
|
231
|
+
) -> Any:
|
|
232
|
+
"""Net interchange (MW) from a balancing authority to neighbors."""
|
|
233
|
+
return _to_df(self._get(
|
|
234
|
+
"/interchange",
|
|
235
|
+
ba=ba.upper(),
|
|
236
|
+
start=_fmt(start),
|
|
237
|
+
end=_fmt(end),
|
|
238
|
+
hours=hours,
|
|
239
|
+
limit=limit,
|
|
240
|
+
))
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------
|
|
243
|
+
# Natural gas
|
|
244
|
+
# ------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
def nat_gas(
|
|
247
|
+
self,
|
|
248
|
+
hub: Optional[str] = None,
|
|
249
|
+
start: Optional[date] = None,
|
|
250
|
+
end: Optional[date] = None,
|
|
251
|
+
days: int = 90,
|
|
252
|
+
limit: int = 5000,
|
|
253
|
+
) -> Any:
|
|
254
|
+
"""Daily natural gas spot prices ($/MMBtu) at major US hubs."""
|
|
255
|
+
return _to_df(self._get(
|
|
256
|
+
"/natural-gas",
|
|
257
|
+
hub=hub,
|
|
258
|
+
start=_fmt(start),
|
|
259
|
+
end=_fmt(end),
|
|
260
|
+
days=days,
|
|
261
|
+
limit=limit,
|
|
262
|
+
))
|
|
263
|
+
|
|
264
|
+
def nat_gas_storage(
|
|
265
|
+
self,
|
|
266
|
+
region: Optional[str] = None,
|
|
267
|
+
start: Optional[date] = None,
|
|
268
|
+
end: Optional[date] = None,
|
|
269
|
+
weeks: int = 52,
|
|
270
|
+
limit: int = 2000,
|
|
271
|
+
) -> Any:
|
|
272
|
+
"""Weekly EIA natural gas in storage (Bcf) by region."""
|
|
273
|
+
return _to_df(self._get(
|
|
274
|
+
"/natural-gas/storage",
|
|
275
|
+
region=region,
|
|
276
|
+
start=_fmt(start),
|
|
277
|
+
end=_fmt(end),
|
|
278
|
+
weeks=weeks,
|
|
279
|
+
limit=limit,
|
|
280
|
+
))
|
|
281
|
+
|
|
282
|
+
# ------------------------------------------------------------------
|
|
283
|
+
# Weather
|
|
284
|
+
# ------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def weather(
|
|
287
|
+
self,
|
|
288
|
+
iso: Optional[str] = None,
|
|
289
|
+
start: Optional[date] = None,
|
|
290
|
+
end: Optional[date] = None,
|
|
291
|
+
hours: int = 24,
|
|
292
|
+
limit: int = 2000,
|
|
293
|
+
) -> Any:
|
|
294
|
+
"""Hourly temperature at representative ISO hub cities."""
|
|
295
|
+
return _to_df(self._get(
|
|
296
|
+
"/weather",
|
|
297
|
+
iso=iso.upper() if iso else None,
|
|
298
|
+
start=_fmt(start),
|
|
299
|
+
end=_fmt(end),
|
|
300
|
+
hours=hours,
|
|
301
|
+
limit=limit,
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# BPA
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
def bpa(
|
|
309
|
+
self,
|
|
310
|
+
start: Optional[date] = None,
|
|
311
|
+
end: Optional[date] = None,
|
|
312
|
+
hours: int = 24,
|
|
313
|
+
limit: int = 2000,
|
|
314
|
+
) -> Any:
|
|
315
|
+
"""BPA 5-min balancing area: wind, hydro, thermal, load."""
|
|
316
|
+
return _to_df(self._get(
|
|
317
|
+
"/bpa",
|
|
318
|
+
start=_fmt(start),
|
|
319
|
+
end=_fmt(end),
|
|
320
|
+
hours=hours,
|
|
321
|
+
limit=limit,
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
# ------------------------------------------------------------------
|
|
325
|
+
# Generator outages
|
|
326
|
+
# ------------------------------------------------------------------
|
|
327
|
+
|
|
328
|
+
def outages(
|
|
329
|
+
self,
|
|
330
|
+
iso: Optional[str] = None,
|
|
331
|
+
outage_type: Optional[str] = None,
|
|
332
|
+
active_only: bool = False,
|
|
333
|
+
days: int = 7,
|
|
334
|
+
limit: int = 2000,
|
|
335
|
+
) -> Any:
|
|
336
|
+
"""Generator outages (unit-level and aggregate) by ISO."""
|
|
337
|
+
return _to_df(self._get(
|
|
338
|
+
"/outages",
|
|
339
|
+
iso=iso.upper() if iso else None,
|
|
340
|
+
outage_type=outage_type,
|
|
341
|
+
active_only=active_only if active_only else None,
|
|
342
|
+
days=days,
|
|
343
|
+
limit=limit,
|
|
344
|
+
))
|
|
345
|
+
|
|
346
|
+
def outages_summary(self, iso: Optional[str] = None) -> Any:
|
|
347
|
+
"""Total MW in outage grouped by ISO × outage type."""
|
|
348
|
+
return _to_df(self._get(
|
|
349
|
+
"/outages/summary",
|
|
350
|
+
iso=iso.upper() if iso else None,
|
|
351
|
+
))
|
|
352
|
+
|
|
353
|
+
# ------------------------------------------------------------------
|
|
354
|
+
# Ancillary services
|
|
355
|
+
# ------------------------------------------------------------------
|
|
356
|
+
|
|
357
|
+
def ancillary(
|
|
358
|
+
self,
|
|
359
|
+
iso: Optional[str] = None,
|
|
360
|
+
market: Optional[str] = None,
|
|
361
|
+
service_type: Optional[str] = None,
|
|
362
|
+
start: Optional[date] = None,
|
|
363
|
+
end: Optional[date] = None,
|
|
364
|
+
hours: int = 24,
|
|
365
|
+
limit: int = 2000,
|
|
366
|
+
) -> Any:
|
|
367
|
+
"""Ancillary service clearing prices and operational capacity."""
|
|
368
|
+
return _to_df(self._get(
|
|
369
|
+
"/ancillary",
|
|
370
|
+
iso=iso.upper() if iso else None,
|
|
371
|
+
market=market.upper() if market else None,
|
|
372
|
+
service_type=service_type,
|
|
373
|
+
start=_fmt(start),
|
|
374
|
+
end=_fmt(end),
|
|
375
|
+
hours=hours,
|
|
376
|
+
limit=limit,
|
|
377
|
+
))
|
|
378
|
+
|
|
379
|
+
def ancillary_latest(self, iso: Optional[str] = None) -> Any:
|
|
380
|
+
"""Latest ancillary snapshot — one row per (ISO, market, service_type)."""
|
|
381
|
+
return _to_df(self._get(
|
|
382
|
+
"/ancillary/latest",
|
|
383
|
+
iso=iso.upper() if iso else None,
|
|
384
|
+
))
|
|
385
|
+
|
|
386
|
+
# ------------------------------------------------------------------
|
|
387
|
+
# Nuclear
|
|
388
|
+
# ------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def nuclear_status(self, iso: Optional[str] = None) -> Any:
|
|
391
|
+
"""Current nuclear unit capacity and output."""
|
|
392
|
+
return _to_df(self._get(
|
|
393
|
+
"/nuclear/status",
|
|
394
|
+
iso=iso.upper() if iso else None,
|
|
395
|
+
))
|
|
396
|
+
|
|
397
|
+
# ------------------------------------------------------------------
|
|
398
|
+
# Emissions
|
|
399
|
+
# ------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
def emissions(
|
|
402
|
+
self,
|
|
403
|
+
iso: Optional[str] = None,
|
|
404
|
+
start: Optional[date] = None,
|
|
405
|
+
end: Optional[date] = None,
|
|
406
|
+
hours: int = 24,
|
|
407
|
+
limit: int = 2000,
|
|
408
|
+
) -> Any:
|
|
409
|
+
"""SO₂, NOₓ, CO₂ emissions by ISO."""
|
|
410
|
+
return _to_df(self._get(
|
|
411
|
+
"/emissions",
|
|
412
|
+
iso=iso.upper() if iso else None,
|
|
413
|
+
start=_fmt(start),
|
|
414
|
+
end=_fmt(end),
|
|
415
|
+
hours=hours,
|
|
416
|
+
limit=limit,
|
|
417
|
+
))
|
|
418
|
+
|
|
419
|
+
# ------------------------------------------------------------------
|
|
420
|
+
# Hydro
|
|
421
|
+
# ------------------------------------------------------------------
|
|
422
|
+
|
|
423
|
+
def hydro(
|
|
424
|
+
self,
|
|
425
|
+
iso: Optional[str] = None,
|
|
426
|
+
start: Optional[date] = None,
|
|
427
|
+
end: Optional[date] = None,
|
|
428
|
+
hours: int = 24,
|
|
429
|
+
limit: int = 2000,
|
|
430
|
+
) -> Any:
|
|
431
|
+
"""Hydro generation and reservoir conditions."""
|
|
432
|
+
return _to_df(self._get(
|
|
433
|
+
"/hydro",
|
|
434
|
+
iso=iso.upper() if iso else None,
|
|
435
|
+
start=_fmt(start),
|
|
436
|
+
end=_fmt(end),
|
|
437
|
+
hours=hours,
|
|
438
|
+
limit=limit,
|
|
439
|
+
))
|
|
440
|
+
|
|
441
|
+
# ------------------------------------------------------------------
|
|
442
|
+
# Solar
|
|
443
|
+
# ------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
def solar(
|
|
446
|
+
self,
|
|
447
|
+
iso: Optional[str] = None,
|
|
448
|
+
start: Optional[date] = None,
|
|
449
|
+
end: Optional[date] = None,
|
|
450
|
+
hours: int = 24,
|
|
451
|
+
limit: int = 2000,
|
|
452
|
+
) -> Any:
|
|
453
|
+
"""Solar generation and curtailment by ISO."""
|
|
454
|
+
return _to_df(self._get(
|
|
455
|
+
"/solar",
|
|
456
|
+
iso=iso.upper() if iso else None,
|
|
457
|
+
start=_fmt(start),
|
|
458
|
+
end=_fmt(end),
|
|
459
|
+
hours=hours,
|
|
460
|
+
limit=limit,
|
|
461
|
+
))
|
|
462
|
+
|
|
463
|
+
# ------------------------------------------------------------------
|
|
464
|
+
# Interconnection queue
|
|
465
|
+
# ------------------------------------------------------------------
|
|
466
|
+
|
|
467
|
+
def queue(
|
|
468
|
+
self,
|
|
469
|
+
iso: Optional[str] = None,
|
|
470
|
+
status: Optional[str] = None,
|
|
471
|
+
fuel_type: Optional[str] = None,
|
|
472
|
+
limit: int = 5000,
|
|
473
|
+
) -> Any:
|
|
474
|
+
"""Generator interconnection queue entries."""
|
|
475
|
+
return _to_df(self._get(
|
|
476
|
+
"/queue",
|
|
477
|
+
iso=iso.upper() if iso else None,
|
|
478
|
+
status=status,
|
|
479
|
+
fuel_type=fuel_type,
|
|
480
|
+
limit=limit,
|
|
481
|
+
))
|
|
482
|
+
|
|
483
|
+
# ------------------------------------------------------------------
|
|
484
|
+
# Commodities
|
|
485
|
+
# ------------------------------------------------------------------
|
|
486
|
+
|
|
487
|
+
def commodities(
|
|
488
|
+
self,
|
|
489
|
+
commodity: Optional[str] = None,
|
|
490
|
+
start: Optional[date] = None,
|
|
491
|
+
end: Optional[date] = None,
|
|
492
|
+
days: int = 90,
|
|
493
|
+
limit: int = 2000,
|
|
494
|
+
) -> Any:
|
|
495
|
+
"""Commodity prices (coal, uranium, carbon credits)."""
|
|
496
|
+
return _to_df(self._get(
|
|
497
|
+
"/commodities",
|
|
498
|
+
commodity=commodity,
|
|
499
|
+
start=_fmt(start),
|
|
500
|
+
end=_fmt(end),
|
|
501
|
+
days=days,
|
|
502
|
+
limit=limit,
|
|
503
|
+
))
|
|
504
|
+
|
|
505
|
+
# ------------------------------------------------------------------
|
|
506
|
+
# Carbon markets
|
|
507
|
+
# ------------------------------------------------------------------
|
|
508
|
+
|
|
509
|
+
def carbon_markets(
|
|
510
|
+
self,
|
|
511
|
+
market: Optional[str] = None,
|
|
512
|
+
start: Optional[date] = None,
|
|
513
|
+
end: Optional[date] = None,
|
|
514
|
+
days: int = 90,
|
|
515
|
+
limit: int = 2000,
|
|
516
|
+
) -> Any:
|
|
517
|
+
"""Carbon credit prices (RGGI, WCI, VCM)."""
|
|
518
|
+
return _to_df(self._get(
|
|
519
|
+
"/carbon-markets",
|
|
520
|
+
market=market,
|
|
521
|
+
start=_fmt(start),
|
|
522
|
+
end=_fmt(end),
|
|
523
|
+
days=days,
|
|
524
|
+
limit=limit,
|
|
525
|
+
))
|
|
526
|
+
|
|
527
|
+
def close(self) -> None:
|
|
528
|
+
self._http.close()
|
|
529
|
+
|
|
530
|
+
def __enter__(self) -> "Client":
|
|
531
|
+
return self
|
|
532
|
+
|
|
533
|
+
def __exit__(self, *_: Any) -> None:
|
|
534
|
+
self.close()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kardashev"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for the Kardashev Labs energy data API"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
dependencies = ["httpx>=0.24"]
|
|
11
|
+
|
|
12
|
+
[project.optional-dependencies]
|
|
13
|
+
pandas = ["pandas>=1.5"]
|
|
14
|
+
dev = ["pytest", "pytest-httpx", "pandas"]
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://kardashevlabs.org"
|
|
18
|
+
API = "https://data.kardashevlabs.org/docs"
|
|
19
|
+
Source = "https://github.com/kardashev-lab/kardashev-py"
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Smoke tests using pytest-httpx to mock the API."""
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
from pytest_httpx import HTTPXMock
|
|
5
|
+
|
|
6
|
+
from kardashev import Client
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import pandas as pd
|
|
10
|
+
_PANDAS = True
|
|
11
|
+
except ImportError:
|
|
12
|
+
_PANDAS = False
|
|
13
|
+
|
|
14
|
+
BASE = "https://data.kardashevlabs.org"
|
|
15
|
+
|
|
16
|
+
FUEL_MIX_RESPONSE = [
|
|
17
|
+
{"ts": "2024-01-01T00:00:00Z", "iso": "CAISO", "fuel_type": "solar", "mw": 5000.0}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _first(result, col: str):
|
|
22
|
+
if _PANDAS:
|
|
23
|
+
return result.iloc[0][col]
|
|
24
|
+
return result[0][col]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _len(result) -> int:
|
|
28
|
+
return len(result)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_fuel_mix(httpx_mock: HTTPXMock):
|
|
32
|
+
httpx_mock.add_response(
|
|
33
|
+
url=f"{BASE}/fuel-mix?iso=CAISO&hours=24&limit=2000",
|
|
34
|
+
json=FUEL_MIX_RESPONSE,
|
|
35
|
+
)
|
|
36
|
+
with Client() as kl:
|
|
37
|
+
result = kl.fuel_mix("CAISO")
|
|
38
|
+
assert _len(result) == 1
|
|
39
|
+
assert _first(result, "iso") == "CAISO"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_fuel_mix_with_dates(httpx_mock: HTTPXMock):
|
|
43
|
+
httpx_mock.add_response(
|
|
44
|
+
url=f"{BASE}/fuel-mix?iso=CAISO&start=2024-01-01&end=2024-01-07&hours=24&limit=2000",
|
|
45
|
+
json=FUEL_MIX_RESPONSE,
|
|
46
|
+
)
|
|
47
|
+
with Client() as kl:
|
|
48
|
+
result = kl.fuel_mix(
|
|
49
|
+
"CAISO",
|
|
50
|
+
start=date(2024, 1, 1),
|
|
51
|
+
end=date(2024, 1, 7),
|
|
52
|
+
)
|
|
53
|
+
assert _len(result) == 1
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_carbon_latest(httpx_mock: HTTPXMock):
|
|
57
|
+
httpx_mock.add_response(
|
|
58
|
+
url=f"{BASE}/carbon/latest",
|
|
59
|
+
json=[{"iso": "CAISO", "ts": "2024-01-01T00:00:00Z", "lbs_co2_per_mwh": 450.0, "total_mw": 30000.0, "pct_clean": 60.0}],
|
|
60
|
+
)
|
|
61
|
+
with Client() as kl:
|
|
62
|
+
result = kl.carbon_latest()
|
|
63
|
+
assert _first(result, "iso") == "CAISO"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_iso_uppercased(httpx_mock: HTTPXMock):
|
|
67
|
+
httpx_mock.add_response(
|
|
68
|
+
url=f"{BASE}/fuel-mix?iso=ERCOT&hours=24&limit=2000",
|
|
69
|
+
json=[],
|
|
70
|
+
)
|
|
71
|
+
with Client() as kl:
|
|
72
|
+
kl.fuel_mix("ercot")
|