windborne 1.0.8__py3-none-any.whl → 1.1.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.
@@ -1,28 +1,84 @@
1
- from .config import (FORECASTS_API_BASE_URL,
2
- FORECASTS_GRIDDED_URL,
3
- FORECASTS_HISTORICAL_URL,
4
- FORECASTS_TCS_URL,
5
- TCS_SUPPORTED_FORMATS)
6
-
7
- from .utils import (make_api_request,
8
- parse_time,
9
- download_and_save_nc,
10
- save_csv_json,
11
- save_as_geojson,
12
- save_as_gpx,
13
- save_as_kml,
14
- save_as_little_r)
1
+ import requests
2
+
3
+ from .utils import (
4
+ parse_time,
5
+ save_arbitrary_response
6
+ )
7
+
8
+ from .api_request import (
9
+ make_api_request
10
+ )
11
+
12
+ from .cyclone_formatting import (
13
+ save_track_as_geojson,
14
+ save_track_as_gpx,
15
+ save_track_as_kml,
16
+ save_track_as_little_r
17
+ )
18
+
19
+ FORECASTS_API_BASE_URL = "https://forecasts.windbornesystems.com/api/v1"
20
+ FORECASTS_GRIDDED_URL = f"{FORECASTS_API_BASE_URL}/gridded"
21
+ FORECASTS_HISTORICAL_URL = f"{FORECASTS_API_BASE_URL}/gridded/historical"
22
+ FORECASTS_TCS_URL = f"{FORECASTS_API_BASE_URL}/tropical_cyclones"
23
+ TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r')
24
+
15
25
 
16
26
  # Point forecasts
17
- def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=None, min_forecast_hour=None, max_forecast_hour=None, initialization_time=None, save_to_file=None):
18
- # Sanitize coordinates by removing whitespace
19
- coordinates = coordinates.replace(" ", "")
27
+ 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):
28
+ """
29
+ Get point forecasts from the API.
20
30
 
21
- params = {"coordinates": coordinates}
31
+ Args:
32
+ coordinates (str, list): Coordinates in the format "latitude,longitude"
33
+ or a list of tuples, lists, or dictionaries with keys 'latitude' and 'longitude'
34
+ min_forecast_time (str, optional): Minimum forecast time in ISO 8601 format (YYYY-MM-DDTHH:00:00)
35
+ max_forecast_time (str, optional): Maximum forecast time in ISO 8601 format (YYYY-MM-DDTHH:00:00)
36
+ min_forecast_hour (int, optional): Minimum forecast hour
37
+ max_forecast_hour (int, optional): Maximum forecast hour
38
+ initialization_time (str, optional): Initialization time in ISO 8601 format (YYYY-MM-DDTHH:00:00)
39
+ output_file (str, optional): Path to save the response data
40
+ Supported formats: .json, .csv
41
+ """
22
42
 
23
- if not coordinates:
43
+ # coordinates should be formatted as a semi-colon separated list of latitude,longitude tuples, eg 37,-121;40.3,-100
44
+ formatted_coordinates = coordinates
45
+
46
+ if isinstance(coordinates, list):
47
+ coordinate_items = []
48
+ for coordinate in coordinates:
49
+ if isinstance(coordinate, tuple) or isinstance(coordinate, list):
50
+ if len(coordinate) != 2:
51
+ print("Coordinates should be tuples or lists with two elements: latitude and longitude.")
52
+ return
53
+
54
+ coordinate_items.append(f"{coordinate[0]},{coordinate[1]}")
55
+ elif isinstance(coordinate, str):
56
+ coordinate_items.append(coordinate)
57
+ elif isinstance(coordinate, dict):
58
+ if 'latitude' in coordinate and 'longitude' in coordinate:
59
+ coordinate_items.append(f"{coordinate['latitude']},{coordinate['longitude']}")
60
+ elif 'lat' in coordinate and 'lon' in coordinate:
61
+ coordinate_items.append(f"{coordinate['lat']},{coordinate['lon']}")
62
+ elif 'lat' in coordinate and 'long' in coordinate:
63
+ coordinate_items.append(f"{coordinate['lat']},{coordinate['long']}")
64
+ elif 'lat' in coordinate and 'lng' in coordinate:
65
+ coordinate_items.append(f"{coordinate['lat']},{coordinate['lng']}")
66
+ else:
67
+ print("Coordinates should be dictionaries with keys 'latitude' and 'longitude'.")
68
+ return
69
+
70
+ formatted_coordinates = ";".join(coordinate_items)
71
+
72
+ formatted_coordinates = formatted_coordinates.replace(" ", "")
73
+
74
+ if not formatted_coordinates or formatted_coordinates == "":
24
75
  print("To get points forecasts you must provide coordinates.")
25
76
  return
77
+
78
+ params = {
79
+ "coordinates": formatted_coordinates
80
+ }
81
+
26
82
  if min_forecast_time:
27
83
  params["min_forecast_time"] = parse_time(min_forecast_time)
28
84
  if max_forecast_time:
@@ -35,234 +91,83 @@ def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=N
35
91
  initialization_time = parse_time(initialization_time,init_time_flag=True)
36
92
  params["initialization_time"] = initialization_time
37
93
 
38
- print("We are initiating handshake procedure with our S3 server.\n")
94
+ print("Generating point forecast...")
39
95
 
40
96
  response = make_api_request(f"{FORECASTS_API_BASE_URL}/points", params=params)
41
97
 
42
- if save_to_file:
43
- # Save as .json
44
- save_csv_json(save_to_file, response, csv_data_key='forecasts')
45
-
46
- return response
47
-
48
- # Gridded forecasts
49
- # We return the whole response, not just the url
50
-
51
- # 500hPa geopotential
52
- # 850hPa geopotential
53
- # 500hPa wind u
54
- # 500hPa wind v
55
- # 500hPa temperature
56
- # 850hPa temperature
57
- # wind_u_10m
58
- # wind_v_10m
59
- # pressure_msl
60
- # temperature_2m
61
-
62
- def get_temperature_2m(time, save_to_file=None):
63
- params = {}
64
-
65
- if not time:
66
- print("To get the gridded output of global 2m temperature forecast you need to provide the time for which to get the forecast.")
67
- return
68
- else:
69
- time_parsed = parse_time(time)
70
- params["time"] = time_parsed
71
-
72
- print("We are initiating handshake procedure with our S3 server.\n")
73
-
74
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/temperature_2m", params=params, return_type='all')
75
-
76
- if save_to_file:
77
- download_and_save_nc(save_to_file, response)
78
-
79
- return response
80
-
81
- # not implemented yet
82
- def get_dewpoint_2m(time, save_to_file=None):
83
- params = {}
84
-
85
- if not time:
86
- print("To get the gridded output of global 2m dewpoint forecast you need to provide the time for which to get the forecast.")
87
- return
88
- else:
89
- time_parsed = parse_time(time)
90
- params["time"] = time_parsed
91
-
92
- print("We are initiating handshake procedure with our S3 server.\n")
93
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/dewpoint_2m", params=params, return_type='all')
94
-
95
- if save_to_file:
96
- download_and_save_nc(save_to_file, response)
97
-
98
- return response
99
-
100
- def get_wind_u_10m(time, save_to_file=None):
101
- params = {}
102
-
103
- if not time:
104
- print("To get the gridded output of global 10m u-component of wind forecasts you need to provide the time for which to get the forecast.")
105
- return
106
- else:
107
- time_parsed = parse_time(time)
108
- params["time"] = time_parsed
109
-
110
- print("We are initiating handshake procedure with our S3 server.\n")
111
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/wind_u_10m", params=params, return_type='all')
112
-
113
- if save_to_file:
114
- download_and_save_nc(save_to_file, response)
115
-
116
- return response
117
-
118
- def get_wind_v_10m(time, save_to_file=None):
119
- params = {}
120
-
121
- if not time:
122
- print("To get the gridded output of global 10m v-component of wind forecasts you need to provide the time for which to get the forecast.")
123
- return
124
- else:
125
- time_parsed = parse_time(time)
126
- params["time"] = time_parsed
127
-
128
- print("We are initiating handshake procedure with our S3 server.\n")
129
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/wind_v_10m", params=params, return_type='all')
130
-
131
- if save_to_file:
132
- download_and_save_nc(save_to_file, response)
98
+ if output_file:
99
+ save_arbitrary_response(output_file, response, csv_data_key='forecasts')
133
100
 
134
101
  return response
135
102
 
136
- def get_500hpa_wind_u(time, save_to_file=None):
137
- params = {}
138
-
139
- if not time:
140
- print("To get the gridded output of global 500hPa wind u-component of wind forecasts you need to provide the time for which to get the forecast.")
141
- return
142
- else:
143
- time_parsed = parse_time(time)
144
- params["time"] = time_parsed
145
-
146
- print("We are initiating handshake procedure with our S3 server.\n")
147
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/500/wind_u", params=params, return_type='all')
148
-
149
- if save_to_file:
150
- download_and_save_nc(save_to_file, response)
151
-
152
- return response
153
-
154
- def get_500hpa_wind_v(time, save_to_file=None):
155
- params = {}
156
-
157
- if not time:
158
- print("To get the gridded output of global 500hPa wind v-component of wind forecasts you need to provide the time for which to get the forecast.")
159
- return
160
- else:
161
- time_parsed = parse_time(time)
162
- params["time"] = time_parsed
163
-
164
- print("We are initiating handshake procedure with our S3 server.\n")
165
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/500/wind_v", params=params, return_type='all')
166
-
167
- if save_to_file:
168
- download_and_save_nc(save_to_file, response)
169
-
170
- return response
171
-
172
- def get_500hpa_temperature(time, save_to_file=None):
173
- params = {}
174
-
175
- if not time:
176
- print("To get the gridded output of global 500hPa temperature forecasts you need to provide the time for which to get the forecast.")
177
- return
178
- else:
179
- time_parsed = parse_time(time)
180
- params["time"] = time_parsed
181
-
182
- print("We are initiating handshake procedure with our S3 server.\n")
183
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/500/temperature", params=params, return_type='all')
184
103
 
185
- if save_to_file:
186
- download_and_save_nc(save_to_file, response)
104
+ def get_gridded_forecast(time, variable, output_file=None):
105
+ """
106
+ Get gridded forecast data from the API.
107
+ Note that this is primarily meant to be used internally by the other functions in this module.
187
108
 
188
- return response
109
+ Args:
110
+ time (str): Date in either ISO 8601 format (YYYY-MM-DDTHH:00:00)
111
+ or compact format (YYYYMMDDHH)
112
+ where HH must be 00, 06, 12, or 18
113
+ variable (str): The variable you want the forecast for
114
+ output_file (str, optional): Path to save the response data
115
+ Supported formats: .nc
116
+ """
189
117
 
190
- def get_850hpa_temperature(time, save_to_file=None):
191
118
  params = {}
192
119
 
193
120
  if not time:
194
- print("To get the gridded output of global 850hPa temperature forecasts you need to provide the time for which to get the forecast.")
121
+ print("Error: the time you want the forecast for is required.")
195
122
  return
196
123
  else:
197
124
  time_parsed = parse_time(time)
198
125
  params["time"] = time_parsed
199
126
 
200
- print("We are initiating handshake procedure with our S3 server.\n")
201
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/850/temperature", params=params, return_type='all')
127
+ response = make_api_request(f"{FORECASTS_GRIDDED_URL}/{variable}", params=params, as_json=False)
202
128
 
203
- if save_to_file:
204
- download_and_save_nc(save_to_file, response)
129
+ if output_file:
130
+ print(f"Output URL found; downloading to {output_file}...")
131
+ download_and_save_output(output_file, response)
205
132
 
206
133
  return response
207
134
 
208
- def get_pressure_msl(time, save_to_file=None):
209
- params = {}
135
+ def get_temperature_2m(time, output_file=None):
136
+ return get_gridded_forecast(time, "temperature_2m", output_file)
210
137
 
211
- if not time:
212
- print("To get the gridded output of global mean sea level pressure forecasts you need to provide the time for which to get the forecast.")
213
- return
214
- else:
215
- time_parsed = parse_time(time)
216
- params["time"] = time_parsed
138
+ # Not yet implemented
139
+ # def get_dewpoint_2m(time, output_file=None):
140
+ # return get_gridded_forecast(time, "dewpoint_2m", output_file)
217
141
 
218
- print("We are initiating handshake procedure with our S3 server.\n")
219
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/pressure_msl", params=params, return_type='all')
142
+ def get_wind_u_10m(time, output_file=None):
143
+ return get_gridded_forecast(time, "wind_u_10m", output_file)
220
144
 
221
- if save_to_file:
222
- download_and_save_nc(save_to_file, response)
145
+ def get_wind_v_10m(time, output_file=None):
146
+ return get_gridded_forecast(time, "wind_v_10m", output_file)
223
147
 
224
- return response
148
+ def get_500hpa_wind_u(time, output_file=None):
149
+ return get_gridded_forecast(time, "500/wind_u", output_file)
225
150
 
226
- def get_500hpa_geopotential(time, save_to_file=None):
227
- params = {}
151
+ def get_500hpa_wind_v(time, output_file=None):
152
+ return get_gridded_forecast(time, "500/wind_v", output_file)
228
153
 
229
- if not time:
230
- print("To get the gridded output of global 500hPa geopotential forecasts you need to provide the time for which to get the forecast.")
231
- return
232
- else:
233
- time_parsed = parse_time(time)
234
- params["time"] = time_parsed
154
+ def get_500hpa_temperature(time, output_file=None):
155
+ return get_gridded_forecast(time, "500/temperature", output_file)
235
156
 
236
- print("We are initiating handshake procedure with our S3 server.\n")
237
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/500/geopotential", params=params, return_type='all')
157
+ def get_850hpa_temperature(time, output_file=None):
158
+ return get_gridded_forecast(time, "850/temperature", output_file)
238
159
 
239
- if save_to_file:
240
- download_and_save_nc(save_to_file, response)
160
+ def get_pressure_msl(time, output_file=None):
161
+ return get_gridded_forecast(time, "pressure_msl", output_file)
241
162
 
242
- return response
163
+ def get_500hpa_geopotential(time, output_file=None):
164
+ return get_gridded_forecast(time, "500/geopotential", output_file)
243
165
 
244
- def get_850hpa_geopotential(time, save_to_file=None):
245
- params = {}
166
+ def get_850hpa_geopotential(time, output_file=None):
167
+ return get_gridded_forecast(time, "850/geopotential", output_file)
246
168
 
247
- if not time:
248
- print("To get the gridded output of global 850hPa geopotential forecasts you need to provide the time for which to get the forecast.")
249
- return
250
- else:
251
- time_parsed = parse_time(time)
252
- params["time"] = time_parsed
253
-
254
- print("We are initiating handshake procedure with our S3 server.\n")
255
- response = make_api_request(f"{FORECASTS_GRIDDED_URL}/850/geopotential", params=params, return_type='all')
256
-
257
- if save_to_file:
258
- download_and_save_nc(save_to_file, response)
259
-
260
- return response
261
169
 
262
- # Historical forecasts
263
- # We return the whole response, not just the url
264
-
265
- def get_historical_temperature_2m(initialization_time, forecast_hour, save_to_file=None):
170
+ def get_historical_output(initialization_time, forecast_hour, variable, output_file=None):
266
171
  params = {}
267
172
 
268
173
  if not initialization_time or not forecast_hour:
@@ -271,88 +176,32 @@ def get_historical_temperature_2m(initialization_time, forecast_hour, save_to_fi
271
176
  "- how many hours after the run time the forecast is valid at.\n")
272
177
  return
273
178
  else:
274
- time_parsed = parse_time(initialization_time, init_time_flag=True)
275
- params["initialization_time"] = time_parsed
276
- params["forecast_hour"] = forecast_hour
277
-
278
- print("We are initiating handshake procedure with our S3 server.\n")
279
-
280
- response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/temperature_2m", params=params, return_type='all')
281
-
282
- if save_to_file:
283
- download_and_save_nc(save_to_file, response)
284
-
285
- return response
286
-
287
- def get_historical_500hpa_geopotential(initialization_time, forecast_hour, save_to_file=None):
288
- params = {}
289
-
290
- if not initialization_time or not forecast_hour:
291
- print("To get the historical output of global 500hPa geopotential forecasts you need to provide:\n"
292
- "- the initialization time of the forecast\n"
293
- "- how many hours after the run time the forecast is valid at.\n")
294
- return
295
- else:
296
- time_parsed = parse_time(initialization_time,init_time_flag=True)
297
- params["initialization_time"] = time_parsed
179
+ params["initialization_time"] = parse_time(initialization_time, init_time_flag=True)
298
180
  params["forecast_hour"] = forecast_hour
299
181
 
300
- print("We are initiating handshake procedure with our S3 server.\n")
301
-
302
- response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/500/geopotential", params=params, return_type='all')
182
+ response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/{variable}", params=params, as_json=False)
303
183
 
304
- if save_to_file:
305
- download_and_save_nc(save_to_file, response)
184
+ if output_file:
185
+ print(f"Output URL found; downloading to {output_file}...")
186
+ download_and_save_output(output_file, response)
306
187
 
307
188
  return response
308
189
 
309
- def get_historical_500hpa_wind_u(initialization_time, forecast_hour, save_to_file=None):
310
- params = {}
311
-
312
- if not initialization_time or not forecast_hour:
313
- print("To get the historical output of global 500hPa wind u forecasts you need to provide:\n"
314
- "- the initialization time of the forecast\n"
315
- "- how many hours after the run time the forecast is valid at.\n")
316
- return
317
- else:
318
- time_parsed = parse_time(initialization_time,init_time_flag=True)
319
- params["initialization_time"] = time_parsed
320
- params["forecast_hour"] = forecast_hour
321
190
 
322
- print("We are initiating handshake procedure with our S3 server.\n")
191
+ def get_historical_temperature_2m(initialization_time, forecast_hour, output_file=None):
192
+ return get_historical_output(initialization_time, forecast_hour, "temperature_2m", output_file)
323
193
 
324
- response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/500/wind_u", params=params, return_type='all')
194
+ def get_historical_500hpa_geopotential(initialization_time, forecast_hour, output_file=None):
195
+ return get_historical_output(initialization_time, forecast_hour, "500/geopotential", output_file)
325
196
 
326
- if save_to_file:
327
- download_and_save_nc(save_to_file, response)
197
+ def get_historical_500hpa_wind_u(initialization_time, forecast_hour, output_file=None):
198
+ return get_historical_output(initialization_time, forecast_hour, "500/wind_u", output_file)
328
199
 
329
- return response
200
+ def get_historical_500hpa_wind_v(initialization_time, forecast_hour, output_file=None):
201
+ return get_historical_output(initialization_time, forecast_hour, "500/wind_v", output_file)
330
202
 
331
- def get_historical_500hpa_wind_v(initialization_time, forecast_hour, save_to_file=None):
332
- params = {}
333
203
 
334
- if not initialization_time or not forecast_hour:
335
- print("To get the historical output of global 500hPa wind v forecasts you need to provide:\n"
336
- "- the initialization time of the forecast\n"
337
- "- how many hours after the run time the forecast is valid at.\n")
338
- return
339
- else:
340
- time_parsed = parse_time(initialization_time, init_time_flag=True)
341
- params["initialization_time"] = time_parsed
342
- params["forecast_hour"] = forecast_hour
343
-
344
- print("We are initiating handshake procedure with our S3 server.\n")
345
-
346
- response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/500/wind_v", params=params, return_type='all')
347
-
348
- if save_to_file:
349
- download_and_save_nc(save_to_file, response)
350
-
351
- return response
352
-
353
- # Other
354
- # TCs
355
- def get_tropical_cyclones(initialization_time=None, basin=None, save_to_file=None):
204
+ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None):
356
205
  """
357
206
  Get tropical cyclone data from the API.
358
207
 
@@ -360,7 +209,7 @@ def get_tropical_cyclones(initialization_time=None, basin=None, save_to_file=Non
360
209
  initialization_time (str): Date in either ISO 8601 format (YYYY-MM-DDTHH:00:00)
361
210
  or compact format (YYYYMMDDHH)
362
211
  where HH must be 00, 06, 12, or 18
363
- save_to_file (str, optional): Path to save the response data
212
+ output_file (str, optional): Path to save the response data
364
213
  Supported formats: .json, .csv, .gpx, .geojson, .kml, .little_r
365
214
 
366
215
  Returns:
@@ -391,12 +240,12 @@ def get_tropical_cyclones(initialization_time=None, basin=None, save_to_file=Non
391
240
  # Response here is a .json
392
241
  response = make_api_request(FORECASTS_TCS_URL, params=params)
393
242
 
394
- if save_to_file:
395
- if '.' not in save_to_file:
243
+ if output_file:
244
+ if '.' not in output_file:
396
245
  print("You have to provide a filetype for your output file.")
397
246
  print_tc_supported_formats()
398
247
  exit (4)
399
- elif not save_to_file.lower().endswith(TCS_SUPPORTED_FORMATS):
248
+ elif not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
400
249
  print("Unsupported file format.")
401
250
  print_tc_supported_formats()
402
251
  exit(44)
@@ -412,7 +261,7 @@ def get_tropical_cyclones(initialization_time=None, basin=None, save_to_file=Non
412
261
  print("-------------------------------------------------------")
413
262
  print("You are too quick!\nThe tropical cyclone data for initialization time are not uploaded yet.")
414
263
  print('You may check again in a few hours again.')
415
- elif save_to_file.lower().endswith('.csv'):
264
+ elif output_file.lower().endswith('.csv'):
416
265
  # Flatten for CSV
417
266
  flattened_data = []
418
267
  for cyclone_id, tracks in response.items():
@@ -424,36 +273,68 @@ def get_tropical_cyclones(initialization_time=None, basin=None, save_to_file=Non
424
273
  'time': track['time']
425
274
  }
426
275
  flattened_data.append(track_data)
427
- save_csv_json(save_to_file, {'prediction': flattened_data}, csv_data_key='prediction')
428
- elif save_to_file.lower().endswith('.json'):
276
+ save_arbitrary_response(output_file, {'prediction': flattened_data}, csv_data_key='prediction')
277
+ elif output_file.lower().endswith('.json'):
429
278
  # Direct save for JSON
430
- save_csv_json(save_to_file, response)
431
- elif save_to_file.lower().endswith('.geojson'):
432
- save_as_geojson(save_to_file, response)
433
- elif save_to_file.lower().endswith('.gpx'):
434
- save_as_gpx(save_to_file, response)
435
- elif save_to_file.lower().endswith('.kml'):
436
- save_as_kml(save_to_file, response)
437
- elif save_to_file.lower().endswith('.little_r'):
438
- save_as_little_r(save_to_file, response)
279
+ save_arbitrary_response(output_file, response)
280
+ elif output_file.lower().endswith('.geojson'):
281
+ save_track_as_geojson(output_file, response)
282
+ elif output_file.lower().endswith('.gpx'):
283
+ save_track_as_gpx(output_file, response)
284
+ elif output_file.lower().endswith('.kml'):
285
+ save_track_as_kml(output_file, response)
286
+ elif output_file.lower().endswith('.little_r'):
287
+ save_track_as_little_r(output_file, response)
439
288
 
440
289
  return response
441
290
 
291
+
442
292
  def get_initialization_times():
443
293
  """
444
- Get available initialization times for pointy.
445
- Returns:
446
- dict: API response data or None if there's an error
294
+ Get available WeatherMesh initialization times (also known as cycle times).
295
+
296
+ Returns dict with keys "latest" and "available" (a list)
447
297
  """
448
298
 
449
- # Response here is a .json
450
299
  response = make_api_request(f"{FORECASTS_API_BASE_URL}/initialization_times.json")
451
300
 
452
301
  return response
453
302
 
303
+
454
304
  # Tropical cyclones
455
305
  def print_tc_supported_formats():
456
306
  """Print supported file formats for saving tcs data."""
457
307
  print("Supported formats:")
458
308
  for fmt in TCS_SUPPORTED_FORMATS:
459
- print(f" - {fmt}")
309
+ print(f" - {fmt}")
310
+
311
+
312
+ def download_and_save_output(output_file, response):
313
+ """
314
+ Downloads a forecast output from a presigned S3 url contained in a response and saves it as a .nc file.
315
+
316
+ Args:
317
+ output_file (str): Path where to save the .nc file
318
+ response (str): Response that contains the S3 url to download the data from
319
+
320
+ Returns:
321
+ bool: True if successful, False otherwise
322
+ """
323
+
324
+ # Add .nc extension if not present
325
+ if not output_file.endswith('.nc'):
326
+ output_file = output_file + '.nc'
327
+
328
+ try:
329
+ # Save the content directly to file
330
+ with open(output_file, 'wb') as f:
331
+ f.write(response.content)
332
+ print(f"Data Successfully saved to {output_file}")
333
+ return True
334
+
335
+ except requests.exceptions.RequestException as e:
336
+ print(f"Error downloading the file: {e}")
337
+ return False
338
+ except Exception as e:
339
+ print(f"Error processing the file: {e}")
340
+ return False