windborne 1.2.8__py3-none-any.whl → 1.3.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.
- windborne/__init__.py +17 -34
- windborne/api_request.py +2 -0
- windborne/cli.py +112 -155
- windborne/data_api.py +46 -32
- windborne/forecasts_api.py +348 -156
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/METADATA +1 -1
- windborne-1.3.0.dist-info/RECORD +13 -0
- windborne-1.2.8.dist-info/RECORD +0 -13
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/WHEEL +0 -0
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/entry_points.txt +0 -0
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/top_level.txt +0 -0
windborne/forecasts_api.py
CHANGED
|
@@ -6,18 +6,109 @@ from .utils import (
|
|
|
6
6
|
print_table
|
|
7
7
|
)
|
|
8
8
|
|
|
9
|
-
from .api_request import make_api_request
|
|
9
|
+
from .api_request import make_api_request, API_BASE_URL
|
|
10
10
|
from .track_formatting import save_track
|
|
11
11
|
|
|
12
|
-
FORECASTS_API_BASE_URL = "
|
|
13
|
-
FORECASTS_GRIDDED_URL = f"{FORECASTS_API_BASE_URL}/gridded"
|
|
14
|
-
FORECASTS_HISTORICAL_URL = f"{FORECASTS_API_BASE_URL}/gridded/historical"
|
|
15
|
-
FORECASTS_TCS_URL = f"{FORECASTS_API_BASE_URL}/tropical_cyclones"
|
|
12
|
+
FORECASTS_API_BASE_URL = f"{API_BASE_URL}/forecasts/v1"
|
|
16
13
|
TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', '.little_r')
|
|
17
14
|
|
|
18
15
|
|
|
16
|
+
# Run information
|
|
17
|
+
def get_run_information(initialization_time=None, ensemble_member=None, print_response=False, model='wm'):
|
|
18
|
+
"""
|
|
19
|
+
Get run information for a given model initialization.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
initialization_time (str, optional): Initialization time (ISO 8601). If omitted, latest is used.
|
|
23
|
+
ensemble_member (str|int, optional): Ensemble member (e.g., "mean" or member number as string/int)
|
|
24
|
+
print_response (bool, optional): Whether to print a formatted summary
|
|
25
|
+
model (str, optional): Forecast model (e.g., wm, wm4, wm4-intra, ecmwf-det)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
dict: API response containing initialization_time, forecast_zero, in_progress, and available list
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
params = {}
|
|
32
|
+
if initialization_time:
|
|
33
|
+
params['initialization_time'] = parse_time(initialization_time, init_time_flag=True)
|
|
34
|
+
if ensemble_member:
|
|
35
|
+
params['ens_member'] = ensemble_member
|
|
36
|
+
|
|
37
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/run_information", params=params)
|
|
38
|
+
|
|
39
|
+
if print_response and response is not None:
|
|
40
|
+
print("Initialization time:", response.get('initialization_time'))
|
|
41
|
+
print("Forecast zero:", response.get('forecast_zero'))
|
|
42
|
+
in_progress = response.get('in_progress')
|
|
43
|
+
if in_progress is not None:
|
|
44
|
+
print("In progress:", in_progress)
|
|
45
|
+
|
|
46
|
+
print("Available forecast hours:")
|
|
47
|
+
available = response.get('available', [])
|
|
48
|
+
for item in available:
|
|
49
|
+
hour = item.get('forecast_hour')
|
|
50
|
+
created_at = item.get('created_at')
|
|
51
|
+
archived = item.get('archived')
|
|
52
|
+
|
|
53
|
+
# eg:
|
|
54
|
+
# - 0 (created at 2025-10-29T00:00:00.000Z, archived)
|
|
55
|
+
# - 0 (created at 2025-10-29T00:00:00.000Z)
|
|
56
|
+
# - 0 (archived)
|
|
57
|
+
|
|
58
|
+
if created_at and archived:
|
|
59
|
+
print(f" - {hour} (created at {created_at}, archived)")
|
|
60
|
+
elif created_at and not archived:
|
|
61
|
+
print(f" - {hour} (created at {created_at})")
|
|
62
|
+
elif not created_at and archived:
|
|
63
|
+
print(f" - {hour} (archived)")
|
|
64
|
+
else:
|
|
65
|
+
print(f" - {hour} (archived)")
|
|
66
|
+
|
|
67
|
+
return response
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Variables
|
|
71
|
+
def get_variables(print_response=False, model='wm'):
|
|
72
|
+
"""
|
|
73
|
+
Get available variables and levels for a given model.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
print_response (bool, optional): Whether to print a formatted summary
|
|
77
|
+
model (str, optional): Forecast model (e.g., wm, wm4, wm4-intra, ecmwf-det)
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
dict: API response containing sfc_variables, upper_variables, and levels
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/variables")
|
|
84
|
+
|
|
85
|
+
if print_response and response is not None:
|
|
86
|
+
sfc = response.get('sfc_variables', [])
|
|
87
|
+
upper = response.get('upper_variables', [])
|
|
88
|
+
levels = response.get('levels', [])
|
|
89
|
+
|
|
90
|
+
print("Surface variables:")
|
|
91
|
+
for v in sfc:
|
|
92
|
+
print(f" - {v}")
|
|
93
|
+
|
|
94
|
+
if len(upper) > 0:
|
|
95
|
+
print("Upper variables:")
|
|
96
|
+
for v in upper:
|
|
97
|
+
print(f" - {v}")
|
|
98
|
+
else:
|
|
99
|
+
print("No upper variables available")
|
|
100
|
+
|
|
101
|
+
if len(levels) > 0:
|
|
102
|
+
print("Levels:")
|
|
103
|
+
for lvl in levels:
|
|
104
|
+
print(f" - {lvl}")
|
|
105
|
+
else:
|
|
106
|
+
print("No levels available")
|
|
107
|
+
|
|
108
|
+
return response
|
|
109
|
+
|
|
19
110
|
# Point forecasts
|
|
20
|
-
def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=None, min_forecast_hour=None, max_forecast_hour=None, initialization_time=None, output_file=None, print_response=False):
|
|
111
|
+
def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=None, min_forecast_hour=None, max_forecast_hour=None, initialization_time=None, output_file=None, print_response=False, model='wm'):
|
|
21
112
|
"""
|
|
22
113
|
Get point forecasts from the API.
|
|
23
114
|
|
|
@@ -88,7 +179,7 @@ def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=N
|
|
|
88
179
|
if print_response:
|
|
89
180
|
print("Generating point forecast...")
|
|
90
181
|
|
|
91
|
-
response = make_api_request(f"{FORECASTS_API_BASE_URL}/
|
|
182
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/point_forecast", params=params)
|
|
92
183
|
|
|
93
184
|
if output_file:
|
|
94
185
|
save_arbitrary_response(output_file, response, csv_data_key='forecasts')
|
|
@@ -107,7 +198,7 @@ def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=N
|
|
|
107
198
|
return response
|
|
108
199
|
|
|
109
200
|
|
|
110
|
-
def get_gridded_forecast(variable, time=None, initialization_time=None, forecast_hour=None, output_file=None, silent=False,
|
|
201
|
+
def get_gridded_forecast(variable, time=None, initialization_time=None, forecast_hour=None, output_file=None, silent=False, ensemble_member=None, model='wm'):
|
|
111
202
|
"""
|
|
112
203
|
Get gridded forecast data from the API.
|
|
113
204
|
Note that this is primarily meant to be used internally by the other functions in this module.
|
|
@@ -141,13 +232,19 @@ def get_gridded_forecast(variable, time=None, initialization_time=None, forecast
|
|
|
141
232
|
elif time:
|
|
142
233
|
params["time"] = parse_time(time)
|
|
143
234
|
|
|
144
|
-
if intracycle:
|
|
145
|
-
params["intracycle"] = intracycle
|
|
146
|
-
|
|
147
235
|
if ensemble_member:
|
|
148
236
|
params["ens_member"] = ensemble_member
|
|
149
237
|
|
|
150
|
-
|
|
238
|
+
# Map variable strings like "500/temperature" to query params variable=temperature, level=500
|
|
239
|
+
request_params = dict(params)
|
|
240
|
+
if '/' in variable and variable.split('/')[0].isdigit():
|
|
241
|
+
level_str, var_name = variable.split('/', 1)
|
|
242
|
+
request_params['variable'] = var_name
|
|
243
|
+
request_params['level'] = int(level_str)
|
|
244
|
+
else:
|
|
245
|
+
request_params['variable'] = variable
|
|
246
|
+
|
|
247
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/gridded", params=request_params, as_json=False)
|
|
151
248
|
|
|
152
249
|
if response is None:
|
|
153
250
|
return None
|
|
@@ -159,56 +256,27 @@ def get_gridded_forecast(variable, time=None, initialization_time=None, forecast
|
|
|
159
256
|
|
|
160
257
|
return response
|
|
161
258
|
|
|
162
|
-
def get_full_gridded_forecast(time, output_file=None):
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def get_temperature_2m(time, output_file=None):
|
|
166
|
-
return get_gridded_forecast(variable="temperature_2m", time=time, output_file=output_file)
|
|
167
|
-
|
|
168
|
-
def get_dewpoint_2m(time, output_file=None):
|
|
169
|
-
return get_gridded_forecast(variable="dewpoint_2m", time=time, output_file=output_file)
|
|
170
|
-
|
|
171
|
-
def get_wind_u_10m(time, output_file=None):
|
|
172
|
-
return get_gridded_forecast(variable="wind_u_10m", time=time, output_file=output_file)
|
|
173
|
-
|
|
174
|
-
def get_wind_v_10m(time, output_file=None):
|
|
175
|
-
return get_gridded_forecast(variable="wind_v_10m", time=time, output_file=output_file)
|
|
176
|
-
|
|
177
|
-
def get_500hpa_wind_u(time, output_file=None):
|
|
178
|
-
return get_gridded_forecast(variable="500/wind_u", time=time, output_file=output_file)
|
|
179
|
-
|
|
180
|
-
def get_500hpa_wind_v(time, output_file=None):
|
|
181
|
-
return get_gridded_forecast(variable="500/wind_v", time=time, output_file=output_file)
|
|
182
|
-
|
|
183
|
-
def get_500hpa_temperature(time, output_file=None):
|
|
184
|
-
return get_gridded_forecast(variable="500/temperature", time=time, output_file=output_file)
|
|
185
|
-
|
|
186
|
-
def get_850hpa_temperature(time, output_file=None):
|
|
187
|
-
return get_gridded_forecast(variable="850/temperature", time=time, output_file=output_file)
|
|
188
|
-
|
|
189
|
-
def get_pressure_msl(time, output_file=None):
|
|
190
|
-
return get_gridded_forecast(variable="pressure_msl", time=time, output_file=output_file)
|
|
191
|
-
|
|
192
|
-
def get_500hpa_geopotential(time, output_file=None):
|
|
193
|
-
return get_gridded_forecast(variable="500/geopotential", time=time, output_file=output_file)
|
|
194
|
-
|
|
195
|
-
def get_850hpa_geopotential(time, output_file=None):
|
|
196
|
-
return get_gridded_forecast(variable="850/geopotential", time=time, output_file=output_file)
|
|
197
|
-
|
|
198
|
-
def get_historical_temperature_2m(initialization_time, forecast_hour, output_file=None):
|
|
199
|
-
return get_gridded_forecast(variable="temperature_2m", initialization_time=initialization_time, forecast_hour=forecast_hour, output_file=output_file)
|
|
200
|
-
|
|
201
|
-
def get_historical_500hpa_geopotential(initialization_time, forecast_hour, output_file=None):
|
|
202
|
-
return get_gridded_forecast(variable="500/geopotential", initialization_time=initialization_time, forecast_hour=forecast_hour, output_file=output_file)
|
|
259
|
+
def get_full_gridded_forecast(time=None, initialization_time=None, forecast_hour=None, output_file=None, silent=False, ensemble_member=None, model='wm'):
|
|
260
|
+
"""
|
|
261
|
+
Get gridded forecast data for all variables from the API.
|
|
203
262
|
|
|
204
|
-
|
|
205
|
-
|
|
263
|
+
Args:
|
|
264
|
+
time (str, optional): Date in either ISO 8601 format (YYYY-MM-DDTHH:00:00)
|
|
265
|
+
or compact format (YYYYMMDDHH). May be used instead of initialization_time and forecast_hour.
|
|
266
|
+
initialization_time (str, optional): Date in either ISO 8601 format (YYYY-MM-DDTHH:00:00)
|
|
267
|
+
or compact format (YYYYMMDDHH). May be used in conjunction with forecast_hour instead of time.
|
|
268
|
+
forecast_hour (int, optional): The forecast hour to get the forecast for. May be used in conjunction with initialization_time instead of time.
|
|
269
|
+
output_file (str, optional): Path to save the response data
|
|
270
|
+
Supported formats: .nc
|
|
271
|
+
silent (bool, optional): Whether to print output
|
|
272
|
+
ensemble_member (int, optional): The ensemble member to get the forecast for
|
|
273
|
+
model (str, optional): The model to get the forecast for
|
|
274
|
+
"""
|
|
206
275
|
|
|
207
|
-
|
|
208
|
-
return get_gridded_forecast(variable="500/wind_v", initialization_time=initialization_time, forecast_hour=forecast_hour, output_file=output_file)
|
|
276
|
+
return get_gridded_forecast(variable="all", time=time, initialization_time=initialization_time, forecast_hour=forecast_hour, output_file=output_file, silent=silent, ensemble_member=ensemble_member, model=model)
|
|
209
277
|
|
|
210
278
|
|
|
211
|
-
def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None, print_response=False):
|
|
279
|
+
def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None, print_response=False, model='wm'):
|
|
212
280
|
"""
|
|
213
281
|
Get tropical cyclone data from the API.
|
|
214
282
|
|
|
@@ -246,7 +314,8 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
|
246
314
|
params["basin"] = basin
|
|
247
315
|
|
|
248
316
|
# Response here is a .json
|
|
249
|
-
|
|
317
|
+
# Tropical cyclones endpoint is model-specific
|
|
318
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/tropical_cyclones", params=params)
|
|
250
319
|
|
|
251
320
|
if output_file:
|
|
252
321
|
if not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
|
|
@@ -262,13 +331,14 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
|
262
331
|
# save_response_to_file() will throw error on saving {}
|
|
263
332
|
elif response is None:
|
|
264
333
|
print("-------------------------------------------------------")
|
|
265
|
-
print("
|
|
266
|
-
print('You may check again in a few hour.')
|
|
334
|
+
print("Tropical cyclones have not yet been generated for this initialization time")
|
|
267
335
|
else:
|
|
268
336
|
save_track(output_file, response, require_ids=True)
|
|
269
337
|
|
|
270
338
|
if print_response:
|
|
271
|
-
if
|
|
339
|
+
if not response:
|
|
340
|
+
print("No tropical cyclones for initialization time:", initialization_time)
|
|
341
|
+
elif len(response) == 0:
|
|
272
342
|
print("No tropical cyclones for initialization time:", initialization_time)
|
|
273
343
|
else:
|
|
274
344
|
print("Tropical Cyclones for initialization time:", initialization_time)
|
|
@@ -279,18 +349,17 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
|
279
349
|
return response
|
|
280
350
|
|
|
281
351
|
|
|
282
|
-
def get_initialization_times(print_response=False, ensemble_member=None,
|
|
352
|
+
def get_initialization_times(print_response=False, ensemble_member=None, model='wm'):
|
|
283
353
|
"""
|
|
284
354
|
Get available WeatherMesh initialization times (also known as cycle times).
|
|
285
355
|
|
|
286
|
-
Returns dict with keys "latest" and "
|
|
356
|
+
Returns dict with keys "latest", "available", and "in_progress"
|
|
287
357
|
"""
|
|
288
358
|
|
|
289
359
|
params = {
|
|
290
360
|
'ens_member': ensemble_member,
|
|
291
|
-
'intracycle': intracycle
|
|
292
361
|
}
|
|
293
|
-
response = make_api_request(f"{FORECASTS_API_BASE_URL}/initialization_times
|
|
362
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/initialization_times", params=params)
|
|
294
363
|
|
|
295
364
|
if print_response:
|
|
296
365
|
print("Latest initialization time:", response['latest'])
|
|
@@ -298,70 +367,30 @@ def get_initialization_times(print_response=False, ensemble_member=None, intracy
|
|
|
298
367
|
for time in response['available']:
|
|
299
368
|
print(f" - {time}")
|
|
300
369
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
def get_historical_initialization_times(print_response=False, ensemble_member=None, intracycle=False):
|
|
305
|
-
"""
|
|
306
|
-
Get historical initialization times for forecasts from our archive
|
|
307
|
-
These may be higher latency to fetch and cannot be used for custom point forecasting
|
|
308
|
-
"""
|
|
309
|
-
params = {
|
|
310
|
-
'ens_member': ensemble_member,
|
|
311
|
-
'intracycle': intracycle
|
|
312
|
-
}
|
|
313
|
-
response = make_api_request(f"{FORECASTS_API_BASE_URL}/historical_initialization_times.json", params=params)
|
|
314
|
-
|
|
315
|
-
if print_response:
|
|
316
|
-
print("Available historical initialization times:")
|
|
317
|
-
for time in response:
|
|
370
|
+
print("In progress initialization times:")
|
|
371
|
+
for time in response['in_progress']:
|
|
318
372
|
print(f" - {time}")
|
|
319
373
|
|
|
320
|
-
|
|
321
|
-
def get_forecast_hours(print_response=False, ensemble_member=None, intracycle=False):
|
|
322
|
-
"""
|
|
323
|
-
Get available forecast hours for WeatherMesh
|
|
324
|
-
This may include initialization times that are not included in the initialization times API that represent outputs
|
|
325
|
-
that are still being generated.
|
|
326
|
-
|
|
327
|
-
Returns dict with keys of initialization times and values of available forecast hours
|
|
328
|
-
"""
|
|
329
|
-
|
|
330
|
-
params = {
|
|
331
|
-
'ens_member': ensemble_member,
|
|
332
|
-
'intracycle': intracycle
|
|
333
|
-
}
|
|
334
|
-
response = make_api_request(f"{FORECASTS_API_BASE_URL}/forecast_hours.json", params=params)
|
|
335
|
-
|
|
336
|
-
if print_response:
|
|
337
|
-
print("Available forecast hours:")
|
|
338
|
-
for time, hours in response.items():
|
|
339
|
-
print(f" - {time}: {', '.join([str(hour) for hour in hours])}")
|
|
340
|
-
|
|
341
374
|
return response
|
|
342
375
|
|
|
343
376
|
|
|
344
|
-
def
|
|
377
|
+
def get_archived_initialization_times(print_response=False, ensemble_member=None, model='wm', page_end=None):
|
|
345
378
|
"""
|
|
346
|
-
Get
|
|
347
|
-
|
|
348
|
-
Returns dict with keys of initialization times and values of dicts, each of which has keys of forecast hours and values of creation times (as ISO strings)
|
|
379
|
+
Get archived initialization times for forecasts from our archive.
|
|
380
|
+
These may be higher latency to fetch and cannot be used for custom point forecasting.
|
|
349
381
|
"""
|
|
350
|
-
|
|
351
382
|
params = {
|
|
352
383
|
'ens_member': ensemble_member,
|
|
353
|
-
'intracycle': intracycle
|
|
354
384
|
}
|
|
355
|
-
|
|
385
|
+
if page_end:
|
|
386
|
+
params['page_end'] = parse_time(page_end)
|
|
387
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/initialization_times/archive", params=params)
|
|
356
388
|
|
|
357
389
|
if print_response:
|
|
358
|
-
print("
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
print(f" - {hour}: {creation_time}")
|
|
363
|
-
|
|
364
|
-
return response
|
|
390
|
+
print("Available archived initialization times:")
|
|
391
|
+
times = response.get('archived_initialization_times', response)
|
|
392
|
+
for time in times:
|
|
393
|
+
print(f" - {time}")
|
|
365
394
|
|
|
366
395
|
|
|
367
396
|
# Tropical cyclones
|
|
@@ -407,78 +436,241 @@ def download_and_save_output(output_file, response, silent=False):
|
|
|
407
436
|
print(f"Error processing the file: {e}")
|
|
408
437
|
return False
|
|
409
438
|
|
|
410
|
-
def get_population_weighted_hdd(initialization_time,
|
|
439
|
+
def get_population_weighted_hdd(initialization_time, ens_member=None, output_file=None, print_response=False, model='wm'):
|
|
411
440
|
"""
|
|
412
441
|
Get population weighted HDD data from the API.
|
|
413
442
|
"""
|
|
414
443
|
params = {
|
|
415
444
|
"initialization_time": initialization_time,
|
|
416
|
-
"intracycle": intracycle,
|
|
417
445
|
"ens_member": ens_member,
|
|
418
|
-
"external_model": external_model
|
|
419
446
|
}
|
|
420
|
-
response = make_api_request(f"{
|
|
447
|
+
response = make_api_request(f"{API_BASE_URL}/insights/v1/{model}/hdds", params=params, as_json=True)
|
|
421
448
|
|
|
422
449
|
if output_file:
|
|
423
450
|
if output_file.endswith('.csv'):
|
|
424
451
|
import csv
|
|
425
452
|
|
|
426
|
-
# save as csv, with a row for each region, and a column for each date, sorted alphabetically by region
|
|
427
|
-
regions = sorted(response['hdd'].keys())
|
|
428
453
|
dates = response['dates']
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
454
|
+
hdd_map = response.get('hdd', {})
|
|
455
|
+
|
|
456
|
+
keys = list(hdd_map.keys())
|
|
457
|
+
is_date_keyed = False
|
|
458
|
+
if len(keys) > 0 and isinstance(keys[0], str):
|
|
459
|
+
import re
|
|
460
|
+
is_date_keyed = re.match(r"^\d{4}-\d{2}-\d{2}", keys[0]) is not None
|
|
461
|
+
|
|
462
|
+
if is_date_keyed:
|
|
463
|
+
region_set = set()
|
|
464
|
+
for date in dates:
|
|
465
|
+
if isinstance(hdd_map.get(date), dict):
|
|
466
|
+
region_set.update(hdd_map[date].keys())
|
|
467
|
+
regions = sorted(region_set)
|
|
468
|
+
with open(output_file, 'w') as f:
|
|
469
|
+
writer = csv.writer(f)
|
|
470
|
+
writer.writerow(['Region'] + dates)
|
|
471
|
+
for region in regions:
|
|
472
|
+
row = [region]
|
|
473
|
+
for date in dates:
|
|
474
|
+
value = ''
|
|
475
|
+
if isinstance(hdd_map.get(date), dict):
|
|
476
|
+
value = hdd_map[date].get(region, '')
|
|
477
|
+
row.append(value)
|
|
478
|
+
writer.writerow(row)
|
|
479
|
+
else:
|
|
480
|
+
regions = sorted(hdd_map.keys())
|
|
481
|
+
with open(output_file, 'w') as f:
|
|
482
|
+
writer = csv.writer(f)
|
|
483
|
+
writer.writerow(['Region'] + dates)
|
|
484
|
+
for region in regions:
|
|
485
|
+
writer.writerow([region] + [hdd_map.get(region, {}).get(date, '') for date in dates])
|
|
437
486
|
|
|
438
487
|
if print_response:
|
|
439
488
|
dates = response['dates']
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
489
|
+
hdd_map = response.get('hdd', {})
|
|
490
|
+
keys = list(hdd_map.keys())
|
|
491
|
+
is_date_keyed = False
|
|
492
|
+
if len(keys) > 0 and isinstance(keys[0], str):
|
|
493
|
+
import re
|
|
494
|
+
is_date_keyed = re.match(r"^\d{4}-\d{2}-\d{2}", keys[0]) is not None
|
|
495
|
+
|
|
496
|
+
if is_date_keyed:
|
|
497
|
+
region_set = set()
|
|
498
|
+
for date in dates:
|
|
499
|
+
if isinstance(hdd_map.get(date), dict):
|
|
500
|
+
region_set.update(hdd_map[date].keys())
|
|
501
|
+
for region in sorted(region_set):
|
|
502
|
+
print(f"{region}:")
|
|
503
|
+
for date in dates:
|
|
504
|
+
if isinstance(hdd_map.get(date), dict) and region in hdd_map[date]:
|
|
505
|
+
print(f" {date}: {hdd_map[date][region]}")
|
|
506
|
+
else:
|
|
507
|
+
for region in sorted(hdd_map.keys()):
|
|
508
|
+
print(f"{region}:")
|
|
509
|
+
for date in dates:
|
|
510
|
+
if isinstance(hdd_map.get(region), dict) and date in hdd_map[region]:
|
|
511
|
+
print(f" {date}: {hdd_map[region][date]}")
|
|
445
512
|
|
|
446
513
|
return response
|
|
447
514
|
|
|
448
|
-
def get_population_weighted_cdd(initialization_time,
|
|
515
|
+
def get_population_weighted_cdd(initialization_time, ens_member=None, output_file=None, print_response=False, model='wm'):
|
|
449
516
|
"""
|
|
450
517
|
Get population weighted CDD data from the API.
|
|
451
518
|
"""
|
|
452
519
|
params = {
|
|
453
520
|
"initialization_time": initialization_time,
|
|
454
|
-
"intracycle": intracycle,
|
|
455
521
|
"ens_member": ens_member,
|
|
456
|
-
"external_model": external_model
|
|
457
522
|
}
|
|
458
|
-
response = make_api_request(f"{
|
|
523
|
+
response = make_api_request(f"{API_BASE_URL}/insights/v1/{model}/cdds", params=params, as_json=True)
|
|
459
524
|
|
|
460
525
|
if output_file:
|
|
461
526
|
if output_file.endswith('.csv'):
|
|
462
527
|
import csv
|
|
463
528
|
|
|
464
|
-
# save as csv, with a row for each region, and a column for each date, sorted alphabetically by region
|
|
465
|
-
regions = sorted(response['cdd'].keys())
|
|
466
529
|
dates = response['dates']
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
530
|
+
cdd_map = response.get('cdd', {})
|
|
531
|
+
|
|
532
|
+
keys = list(cdd_map.keys())
|
|
533
|
+
is_date_keyed = False
|
|
534
|
+
if len(keys) > 0 and isinstance(keys[0], str):
|
|
535
|
+
import re
|
|
536
|
+
is_date_keyed = re.match(r"^\d{4}-\d{2}-\d{2}", keys[0]) is not None
|
|
537
|
+
|
|
538
|
+
if is_date_keyed:
|
|
539
|
+
region_set = set()
|
|
540
|
+
for date in dates:
|
|
541
|
+
if isinstance(cdd_map.get(date), dict):
|
|
542
|
+
region_set.update(cdd_map[date].keys())
|
|
543
|
+
regions = sorted(region_set)
|
|
544
|
+
with open(output_file, 'w') as f:
|
|
545
|
+
writer = csv.writer(f)
|
|
546
|
+
writer.writerow(['Region'] + dates)
|
|
547
|
+
for region in regions:
|
|
548
|
+
row = [region]
|
|
549
|
+
for date in dates:
|
|
550
|
+
value = ''
|
|
551
|
+
if isinstance(cdd_map.get(date), dict):
|
|
552
|
+
value = cdd_map[date].get(region, '')
|
|
553
|
+
row.append(value)
|
|
554
|
+
writer.writerow(row)
|
|
555
|
+
else:
|
|
556
|
+
regions = sorted(cdd_map.keys())
|
|
557
|
+
with open(output_file, 'w') as f:
|
|
558
|
+
writer = csv.writer(f)
|
|
559
|
+
writer.writerow(['Region'] + dates)
|
|
560
|
+
for region in regions:
|
|
561
|
+
writer.writerow([region] + [cdd_map.get(region, {}).get(date, '') for date in dates])
|
|
475
562
|
|
|
476
563
|
if print_response:
|
|
477
564
|
dates = response['dates']
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
565
|
+
cdd_map = response.get('cdd', {})
|
|
566
|
+
keys = list(cdd_map.keys())
|
|
567
|
+
is_date_keyed = False
|
|
568
|
+
if len(keys) > 0 and isinstance(keys[0], str):
|
|
569
|
+
import re
|
|
570
|
+
is_date_keyed = re.match(r"^\d{4}-\d{2}-\d{2}", keys[0]) is not None
|
|
571
|
+
|
|
572
|
+
if is_date_keyed:
|
|
573
|
+
region_set = set()
|
|
574
|
+
for date in dates:
|
|
575
|
+
if isinstance(cdd_map.get(date), dict):
|
|
576
|
+
region_set.update(cdd_map[date].keys())
|
|
577
|
+
for region in sorted(region_set):
|
|
578
|
+
print(f"{region}:")
|
|
579
|
+
for date in dates:
|
|
580
|
+
if isinstance(cdd_map.get(date), dict) and region in cdd_map[date]:
|
|
581
|
+
print(f" {date}: {cdd_map[date][region]}")
|
|
582
|
+
else:
|
|
583
|
+
for region in sorted(cdd_map.keys()):
|
|
584
|
+
print(f"{region}:")
|
|
585
|
+
for date in dates:
|
|
586
|
+
if isinstance(cdd_map.get(region), dict) and date in cdd_map[region]:
|
|
587
|
+
print(f" {date}: {cdd_map[region][date]}")
|
|
483
588
|
|
|
484
589
|
return response
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def get_point_forecasts_interpolated(coordinates, min_forecast_time=None, max_forecast_time=None, min_forecast_hour=None, max_forecast_hour=None, initialization_time=None, ensemble_member=None, output_file=None, print_response=False, model='wm'):
|
|
593
|
+
"""
|
|
594
|
+
Get interpolated point forecasts from the API.
|
|
595
|
+
|
|
596
|
+
Args:
|
|
597
|
+
coordinates (str | list): "lat,lon;lat,lon" string or list of tuples/strings/dicts
|
|
598
|
+
min_forecast_time (str, optional): Minimum forecast time (ISO 8601 formats supported)
|
|
599
|
+
max_forecast_time (str, optional): Maximum forecast time (ISO 8601 formats supported)
|
|
600
|
+
min_forecast_hour (int, optional): Minimum forecast hour
|
|
601
|
+
max_forecast_hour (int, optional): Maximum forecast hour
|
|
602
|
+
initialization_time (str, optional): Initialization time (ISO 8601). If omitted, latest is used
|
|
603
|
+
ensemble_member (str | int, optional): Ensemble member (e.g., "mean" or 0-23)
|
|
604
|
+
output_file (str, optional): Save response to .json or .csv (csv_data_key='forecasts')
|
|
605
|
+
print_response (bool, optional): Print response summary to stdout
|
|
606
|
+
model (str, optional): Forecast model (e.g., wm, wm4, wm4-intra, ecmwf-det)
|
|
607
|
+
"""
|
|
608
|
+
|
|
609
|
+
formatted_coordinates = coordinates
|
|
610
|
+
|
|
611
|
+
if isinstance(coordinates, list):
|
|
612
|
+
coordinate_items = []
|
|
613
|
+
for coordinate in coordinates:
|
|
614
|
+
if isinstance(coordinate, (tuple, list)):
|
|
615
|
+
if len(coordinate) != 2:
|
|
616
|
+
print("Coordinates should be tuples or lists with two elements: latitude and longitude.")
|
|
617
|
+
return
|
|
618
|
+
coordinate_items.append(f"{coordinate[0]},{coordinate[1]}")
|
|
619
|
+
elif isinstance(coordinate, str):
|
|
620
|
+
coordinate_items.append(coordinate)
|
|
621
|
+
elif isinstance(coordinate, dict):
|
|
622
|
+
if 'latitude' in coordinate and 'longitude' in coordinate:
|
|
623
|
+
coordinate_items.append(f"{coordinate['latitude']},{coordinate['longitude']}")
|
|
624
|
+
elif 'lat' in coordinate and 'lon' in coordinate:
|
|
625
|
+
coordinate_items.append(f"{coordinate['lat']},{coordinate['lon']}")
|
|
626
|
+
elif 'lat' in coordinate and 'long' in coordinate:
|
|
627
|
+
coordinate_items.append(f"{coordinate['lat']},{coordinate['long']}")
|
|
628
|
+
elif 'lat' in coordinate and 'lng' in coordinate:
|
|
629
|
+
coordinate_items.append(f"{coordinate['lat']},{coordinate['lng']}")
|
|
630
|
+
else:
|
|
631
|
+
print("Coordinates should be dictionaries with keys 'latitude' and 'longitude'.")
|
|
632
|
+
return
|
|
633
|
+
formatted_coordinates = ";".join(coordinate_items)
|
|
634
|
+
|
|
635
|
+
formatted_coordinates = formatted_coordinates.replace(" ", "")
|
|
636
|
+
if not formatted_coordinates:
|
|
637
|
+
print("To get interpolated points forecasts you must provide coordinates.")
|
|
638
|
+
return
|
|
639
|
+
|
|
640
|
+
params = {"coordinates": formatted_coordinates}
|
|
641
|
+
|
|
642
|
+
if min_forecast_time:
|
|
643
|
+
params["min_forecast_time"] = parse_time(min_forecast_time)
|
|
644
|
+
if max_forecast_time:
|
|
645
|
+
params["max_forecast_time"] = parse_time(max_forecast_time)
|
|
646
|
+
if min_forecast_hour:
|
|
647
|
+
params["min_forecast_hour"] = int(min_forecast_hour)
|
|
648
|
+
if max_forecast_hour:
|
|
649
|
+
params["max_forecast_hour"] = int(max_forecast_hour)
|
|
650
|
+
if initialization_time:
|
|
651
|
+
params["initialization_time"] = parse_time(initialization_time, init_time_flag=True)
|
|
652
|
+
if ensemble_member is not None:
|
|
653
|
+
params["ens_member"] = ensemble_member
|
|
654
|
+
|
|
655
|
+
if print_response:
|
|
656
|
+
print("Generating interpolated point forecast...")
|
|
657
|
+
|
|
658
|
+
# Note: interpolated endpoint uses underscore path: point_forecast/interpolated
|
|
659
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/{model}/point_forecast/interpolated", params=params)
|
|
660
|
+
|
|
661
|
+
if output_file:
|
|
662
|
+
save_arbitrary_response(output_file, response, csv_data_key='forecasts')
|
|
663
|
+
|
|
664
|
+
if print_response and response is not None:
|
|
665
|
+
unformatted_coordinates = formatted_coordinates.split(';')
|
|
666
|
+
# Include latitude/longitude along with standard surface variables
|
|
667
|
+
keys = ['time', 'temperature_2m', 'dewpoint_2m', 'wind_u_10m', 'wind_v_10m', 'precipitation', 'pressure_msl', 'latitude', 'longitude']
|
|
668
|
+
headers = ['Time', '2m Temperature (°C)', '2m Dewpoint (°C)', 'Wind U (m/s)', 'Wind V (m/s)', 'Precipitation (mm)', 'MSL Pressure (hPa)', 'Latitude', 'Longitude']
|
|
669
|
+
|
|
670
|
+
forecasts = response.get('forecasts', [])
|
|
671
|
+
for i in range(min(len(forecasts), len(unformatted_coordinates))):
|
|
672
|
+
latitude, longitude = unformatted_coordinates[i].split(',')
|
|
673
|
+
print(f"\nForecast for ({latitude}, {longitude})")
|
|
674
|
+
print_table(forecasts[i], keys=keys, headers=headers)
|
|
675
|
+
|
|
676
|
+
return response
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
windborne/__init__.py,sha256=UQLEVUvRS4Es8unszEVVBS_744be3GSNPcomeDvu_3c,1709
|
|
2
|
+
windborne/api_request.py,sha256=phPCgm40JwAn1bf6osMOH40Sw_OxmlH_gx-W7KxYmtg,7949
|
|
3
|
+
windborne/cli.py,sha256=ToYo_DxhYEjeYET6RtAaZbdgEJnMkbwVI1ynwtOrYj4,30484
|
|
4
|
+
windborne/data_api.py,sha256=saIS5NsoNWEwsyL6uREP-SvwHyf-ngZLTbRJoWmEGMw,34582
|
|
5
|
+
windborne/forecasts_api.py,sha256=Zp4wtUQI0F4pIbAHirhfJzTpBthCFraUKH8S8FpNkaE,29568
|
|
6
|
+
windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
|
|
7
|
+
windborne/track_formatting.py,sha256=LaLfTyjpWoOtHmReJPLViY0MKm_iPL_5I2OB_lNvGGA,10054
|
|
8
|
+
windborne/utils.py,sha256=H8gvZ4Lrr0UmLl25iMZs6NsZliCY_73Ved_rBIqxJg4,7240
|
|
9
|
+
windborne-1.3.0.dist-info/METADATA,sha256=ZZ3ddTk7ZY-qnJxspEKAQm2v9Ub6ISrWZHyLg_-KagM,1304
|
|
10
|
+
windborne-1.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
windborne-1.3.0.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
|
12
|
+
windborne-1.3.0.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
|
13
|
+
windborne-1.3.0.dist-info/RECORD,,
|