windborne 1.2.7__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.
@@ -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 = "https://forecasts.windbornesystems.com/api/v1"
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}/points", params=params)
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, intracycle=False, ensemble_member=None):
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
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/{variable}", params=params, as_json=False)
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
- return get_gridded_forecast(variable="FULL", time=time, output_file=output_file)
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
- def get_historical_500hpa_wind_u(initialization_time, forecast_hour, output_file=None):
205
- return get_gridded_forecast(variable="500/wind_u", initialization_time=initialization_time, forecast_hour=forecast_hour, output_file=output_file)
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
- def get_historical_500hpa_wind_v(initialization_time, forecast_hour, output_file=None):
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
- response = make_api_request(FORECASTS_TCS_URL, params=params)
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("You are too quick!\nThe tropical cyclone data for initialization time are not uploaded yet.")
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 len(response) == 0:
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, intracycle=False):
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 "available"
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.json", params=params)
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
- return response
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 get_generation_times(print_response=False, ensemble_member=None, intracycle=False):
377
+ def get_archived_initialization_times(print_response=False, ensemble_member=None, model='wm', page_end=None):
345
378
  """
346
- Get the creation time for each forecast hour output file.
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
- response = make_api_request(f"{FORECASTS_API_BASE_URL}/generation_times.json", params=params)
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("Generation times:")
359
- for time, hours in response.items():
360
- print(f" - {time}:")
361
- for hour, creation_time in hours.items():
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, intracycle=False, ens_member=None, external_model=None, output_file=None, print_response=False):
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"{FORECASTS_API_BASE_URL}/hdd", params=params, as_json=True)
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
- data = [[response['hdd'][region][dates[i]] for region in regions] for i in range(len(dates))]
430
-
431
- with open(output_file, 'w') as f:
432
- writer = csv.writer(f)
433
- writer.writerow(['Region'] + dates)
434
-
435
- for region in regions:
436
- writer.writerow([region] + [response['hdd'][region][date] for date in dates])
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
- print(response['hdd']['Alabama'])
441
- for region in sorted(response['hdd'].keys()):
442
- print(f"{region}:")
443
- for i in range(len(dates)):
444
- print(f" {dates[i]}: {response['hdd'][region][dates[i]]}")
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, intracycle=False, ens_member=None, external_model=None, output_file=None, print_response=False):
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"{FORECASTS_API_BASE_URL}/cdd", params=params, as_json=True)
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
- data = [[response['cdd'][region][dates[i]] for region in regions] for i in range(len(dates))]
468
-
469
- with open(output_file, 'w') as f:
470
- writer = csv.writer(f)
471
- writer.writerow(['Region'] + dates)
472
-
473
- for region in regions:
474
- writer.writerow([region] + [response['cdd'][region][date] for date in dates])
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
- print(response['cdd']['Alabama'])
479
- for region in sorted(response['cdd'].keys()):
480
- print(f"{region}:")
481
- for i in range(len(dates)):
482
- print(f" {dates[i]}: {response['cdd'][region][dates[i]]}")
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: windborne
3
- Version: 1.2.7
3
+ Version: 1.3.0
4
4
  Summary: A Python library for interacting with WindBorne Data and Forecasts API
5
5
  Author-email: WindBorne Systems <data@windbornesystems.com>
6
6
  Classifier: Programming Language :: Python :: 3
@@ -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,,