windborne 1.1.1__py3-none-any.whl → 1.1.2__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/api_request.py +1 -2
- windborne/cli.py +13 -27
- windborne/data_api.py +2 -17
- windborne/forecasts_api.py +43 -6
- windborne/utils.py +34 -5
- {windborne-1.1.1.dist-info → windborne-1.1.2.dist-info}/METADATA +1 -1
- windborne-1.1.2.dist-info/RECORD +13 -0
- windborne-1.1.1.dist-info/RECORD +0 -13
- {windborne-1.1.1.dist-info → windborne-1.1.2.dist-info}/WHEEL +0 -0
- {windborne-1.1.1.dist-info → windborne-1.1.2.dist-info}/entry_points.txt +0 -0
- {windborne-1.1.1.dist-info → windborne-1.1.2.dist-info}/top_level.txt +0 -0
windborne/api_request.py
CHANGED
@@ -199,8 +199,7 @@ def make_api_request(url, params=None, as_json=True, retry_counter=0):
|
|
199
199
|
mission_id = url.split('/missions/')[1].split('/')[0]
|
200
200
|
print(f"Mission ID provided: {mission_id}")
|
201
201
|
print(f"No mission found with id: {mission_id}")
|
202
|
-
|
203
|
-
|
202
|
+
return None
|
204
203
|
elif http_err.response.status_code == 502:
|
205
204
|
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
206
205
|
print(f"Underlying error: 502 Bad Gateway")
|
windborne/cli.py
CHANGED
@@ -139,7 +139,7 @@ def main():
|
|
139
139
|
points_parser.add_argument('-mh','--min-hour', type=int, help='Minimum forecast hour')
|
140
140
|
points_parser.add_argument('-xh','--max-hour', type=int, help='Maximum forecast hour')
|
141
141
|
points_parser.add_argument('-i', '--init-time', help='Initialization time')
|
142
|
-
points_parser.add_argument('output_file', help='Output file')
|
142
|
+
points_parser.add_argument('output_file', nargs='?', help='Output file')
|
143
143
|
|
144
144
|
# GRIDDED FORECASTS
|
145
145
|
####################################################################################################################
|
@@ -210,9 +210,9 @@ def main():
|
|
210
210
|
####################################################################################################################
|
211
211
|
|
212
212
|
# Tropical Cyclones Command
|
213
|
-
|
214
|
-
|
215
|
-
|
213
|
+
tropical_cyclones_parser = subparsers.add_parser('tropical_cyclones', help='Get tropical cyclone forecasts')
|
214
|
+
tropical_cyclones_parser.add_argument('-b', '--basin', help='Optional: filter tropical cyclones on basin[ NA, EP, WP, NI, SI, AU, SP]')
|
215
|
+
tropical_cyclones_parser.add_argument('args', nargs='*',
|
216
216
|
help='[optional: initialization time (YYYYMMDDHH, YYYY-MM-DDTHH, or YYYY-MM-DDTHH:mm:ss)] output_file')
|
217
217
|
|
218
218
|
# Initialization Times Command
|
@@ -396,15 +396,12 @@ def main():
|
|
396
396
|
min_forecast_hour=min_forecast_hour,
|
397
397
|
max_forecast_hour=max_forecast_hour,
|
398
398
|
initialization_time=initialization_time,
|
399
|
-
output_file=args.output_file
|
399
|
+
output_file=args.output_file,
|
400
|
+
print_response=(not args.output_file)
|
400
401
|
)
|
401
402
|
|
402
403
|
elif args.command == 'init_times':
|
403
|
-
|
404
|
-
print("Available initialization times for point forecasts:\n")
|
405
|
-
pprint(get_initialization_times())
|
406
|
-
else:
|
407
|
-
print("We can't currently display available initialization times for point forecasts:\n")
|
404
|
+
get_initialization_times(print_response=True)
|
408
405
|
|
409
406
|
elif args.command == 'grid_temp_2m':
|
410
407
|
# Parse grid_temp_2m arguments
|
@@ -589,39 +586,28 @@ def main():
|
|
589
586
|
print("Too many arguments")
|
590
587
|
print("\nUsage: windborne hist_500hpa_wind_v initialization_time forecast_hour output_file")
|
591
588
|
|
592
|
-
elif args.command == '
|
589
|
+
elif args.command == 'tropical_cyclones':
|
593
590
|
# Parse cyclones arguments
|
594
|
-
basin_name = '
|
591
|
+
basin_name = 'all basins'
|
595
592
|
if args.basin:
|
596
593
|
basin_name = f"{args.basin} basin"
|
597
|
-
print(f"Checking for tropical cyclones only within {args.basin} basin\n")
|
598
594
|
|
599
595
|
if len(args.args) == 0:
|
600
|
-
|
601
|
-
|
602
|
-
print(f"Found {len(get_tropical_cyclones())} cyclone(s)\n")
|
603
|
-
pprint(get_tropical_cyclones(basin=args.basin))
|
604
|
-
return
|
605
|
-
else:
|
606
|
-
print("There are no active tropical cyclones for our latest available initialization time.")
|
596
|
+
get_tropical_cyclones(basin=args.basin, print_response=True)
|
597
|
+
return
|
607
598
|
elif len(args.args) == 1:
|
608
599
|
if '.' in args.args[0]:
|
609
600
|
# Save tcs with the latest available initialization time in filename
|
610
601
|
get_tropical_cyclones(basin=args.basin, output_file=args.args[0])
|
611
602
|
else:
|
612
603
|
# Display tcs for selected initialization time
|
613
|
-
|
614
|
-
print(f"Loading tropical cyclones for initialization time {args.args[0]}\n")
|
615
|
-
print(f"Found {len(get_tropical_cyclones(initialization_time=args.args[0]))} cyclone(s)\n")
|
616
|
-
pprint(get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin))
|
617
|
-
else:
|
618
|
-
print(f"No active tropical cyclones for {basin_name} and {args.args[0]} initialization time.")
|
604
|
+
get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, print_response=True)
|
619
605
|
elif len(args.args) == 2:
|
620
606
|
print(f"Saving tropical cyclones for initialization time {args.args[0]} and {basin_name}\n")
|
621
607
|
get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, output_file=args.args[1])
|
622
608
|
else:
|
623
609
|
print("Error: Too many arguments")
|
624
|
-
print("Usage: windborne
|
610
|
+
print("Usage: windborne tropical_cyclones [initialization_time] output_file")
|
625
611
|
|
626
612
|
else:
|
627
613
|
parser.print_help()
|
windborne/data_api.py
CHANGED
@@ -6,7 +6,7 @@ import json
|
|
6
6
|
|
7
7
|
from .api_request import make_api_request
|
8
8
|
from .observation_formatting import format_little_r, convert_to_netcdf
|
9
|
-
from .utils import to_unix_timestamp, save_arbitrary_response
|
9
|
+
from .utils import to_unix_timestamp, save_arbitrary_response, print_table
|
10
10
|
|
11
11
|
DATA_API_BASE_URL = "https://sensor-data.windbornesystems.com/api/v1"
|
12
12
|
|
@@ -645,22 +645,7 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
645
645
|
if flying_missions:
|
646
646
|
print("\nCurrently flying missions:\n")
|
647
647
|
|
648
|
-
|
649
|
-
headers = ["Index", "Mission ID", "Mission Name"]
|
650
|
-
rows = [
|
651
|
-
[str(i), mission.get("id", "N/A"), mission.get("name", "Unnamed Mission")]
|
652
|
-
for i, mission in enumerate(flying_missions, start=1)
|
653
|
-
]
|
654
|
-
|
655
|
-
# Kinda overkill | but it's a good practice if we ever change missions naming convention
|
656
|
-
# Calculate column widths
|
657
|
-
col_widths = [max(len(cell) for cell in col) + 2 for col in zip(headers, *rows)]
|
658
|
-
|
659
|
-
# Display table
|
660
|
-
print("".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers))))
|
661
|
-
print("".join("-" * col_width for col_width in col_widths))
|
662
|
-
for row in rows:
|
663
|
-
print("".join(f"{row[i]:<{col_widths[i]}}" for i in range(len(row))))
|
648
|
+
print_table(flying_missions, keys=['i', 'id', 'name'], headers=['Index', 'Mission ID', 'Mission Name'])
|
664
649
|
else:
|
665
650
|
print("No missions are currently flying.")
|
666
651
|
return
|
windborne/forecasts_api.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
import requests
|
2
|
+
import json
|
2
3
|
|
3
4
|
from .utils import (
|
4
5
|
parse_time,
|
5
|
-
save_arbitrary_response
|
6
|
+
save_arbitrary_response,
|
7
|
+
print_table
|
6
8
|
)
|
7
9
|
|
8
10
|
from .api_request import (
|
@@ -24,7 +26,7 @@ TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r'
|
|
24
26
|
|
25
27
|
|
26
28
|
# Point forecasts
|
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):
|
29
|
+
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):
|
28
30
|
"""
|
29
31
|
Get point forecasts from the API.
|
30
32
|
|
@@ -38,6 +40,7 @@ def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=N
|
|
38
40
|
initialization_time (str, optional): Initialization time in ISO 8601 format (YYYY-MM-DDTHH:00:00)
|
39
41
|
output_file (str, optional): Path to save the response data
|
40
42
|
Supported formats: .json, .csv
|
43
|
+
print_response (bool, optional): Whether to print the response data
|
41
44
|
"""
|
42
45
|
|
43
46
|
# coordinates should be formatted as a semi-colon separated list of latitude,longitude tuples, eg 37,-121;40.3,-100
|
@@ -91,13 +94,25 @@ def get_point_forecasts(coordinates, min_forecast_time=None, max_forecast_time=N
|
|
91
94
|
initialization_time = parse_time(initialization_time,init_time_flag=True)
|
92
95
|
params["initialization_time"] = initialization_time
|
93
96
|
|
94
|
-
|
97
|
+
if print_response:
|
98
|
+
print("Generating point forecast...")
|
95
99
|
|
96
100
|
response = make_api_request(f"{FORECASTS_API_BASE_URL}/points", params=params)
|
97
101
|
|
98
102
|
if output_file:
|
99
103
|
save_arbitrary_response(output_file, response, csv_data_key='forecasts')
|
100
104
|
|
105
|
+
if print_response:
|
106
|
+
unformatted_coordinates = formatted_coordinates.split(';')
|
107
|
+
|
108
|
+
keys = ['time', 'temperature_2m', 'dewpoint_2m', 'wind_u_10m', 'wind_v_10m', 'precipitation', 'pressure_msl']
|
109
|
+
headers = ['Time', '2m Temperature (°C)', '2m Dewpoint (°C)', 'Wind U (m/s)', 'Wind V (m/s)', 'Precipitation (mm)', 'MSL Pressure (hPa)']
|
110
|
+
|
111
|
+
for i in range(len(response['forecasts'])):
|
112
|
+
latitude, longitude = unformatted_coordinates[i].split(',')
|
113
|
+
print(f"\nForecast for ({latitude}, {longitude})")
|
114
|
+
print_table(response['forecasts'][i], keys=keys, headers=headers)
|
115
|
+
|
101
116
|
return response
|
102
117
|
|
103
118
|
|
@@ -126,6 +141,9 @@ def get_gridded_forecast(time, variable, output_file=None):
|
|
126
141
|
|
127
142
|
response = make_api_request(f"{FORECASTS_GRIDDED_URL}/{variable}", params=params, as_json=False)
|
128
143
|
|
144
|
+
if response is None:
|
145
|
+
return None
|
146
|
+
|
129
147
|
if output_file:
|
130
148
|
print(f"Output URL found; downloading to {output_file}...")
|
131
149
|
download_and_save_output(output_file, response)
|
@@ -181,6 +199,9 @@ def get_historical_output(initialization_time, forecast_hour, variable, output_f
|
|
181
199
|
|
182
200
|
response = make_api_request(f"{FORECASTS_HISTORICAL_URL}/{variable}", params=params, as_json=False)
|
183
201
|
|
202
|
+
if response is None:
|
203
|
+
return None
|
204
|
+
|
184
205
|
if output_file:
|
185
206
|
print(f"Output URL found; downloading to {output_file}...")
|
186
207
|
download_and_save_output(output_file, response)
|
@@ -201,7 +222,7 @@ def get_historical_500hpa_wind_v(initialization_time, forecast_hour, output_file
|
|
201
222
|
return get_historical_output(initialization_time, forecast_hour, "500/wind_v", output_file)
|
202
223
|
|
203
224
|
|
204
|
-
def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None):
|
225
|
+
def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None, print_response=False):
|
205
226
|
"""
|
206
227
|
Get tropical cyclone data from the API.
|
207
228
|
|
@@ -209,8 +230,10 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
209
230
|
initialization_time (str): Date in either ISO 8601 format (YYYY-MM-DDTHH:00:00)
|
210
231
|
or compact format (YYYYMMDDHH)
|
211
232
|
where HH must be 00, 06, 12, or 18
|
233
|
+
basin (str, optional): Basin code (e.g., 'NA', 'EP', 'WP', 'NI', 'SI', 'AU', 'SP')
|
212
234
|
output_file (str, optional): Path to save the response data
|
213
235
|
Supported formats: .json, .csv, .gpx, .geojson, .kml, .little_r
|
236
|
+
print_response (bool, optional): Whether to print the response data
|
214
237
|
|
215
238
|
Returns:
|
216
239
|
dict: API response data or None if there's an error
|
@@ -221,7 +244,6 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
221
244
|
initialization_time_parsed = parse_time(initialization_time, init_time_flag=True)
|
222
245
|
params["initialization_time"] = initialization_time_parsed
|
223
246
|
else:
|
224
|
-
# Madee this for our displaying message when no active tcs found
|
225
247
|
initialization_time = 'latest'
|
226
248
|
|
227
249
|
if basin:
|
@@ -286,10 +308,19 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
|
|
286
308
|
elif output_file.lower().endswith('.little_r'):
|
287
309
|
save_track_as_little_r(output_file, response)
|
288
310
|
|
311
|
+
if print_response:
|
312
|
+
if len(response) == 0:
|
313
|
+
print("No tropical cyclones for initialization time:", initialization_time)
|
314
|
+
else:
|
315
|
+
print("Tropical Cyclones for initialization time:", initialization_time)
|
316
|
+
for cyclone_id, tracks in response.items():
|
317
|
+
print(f"\nCyclone ID: {cyclone_id}")
|
318
|
+
print_table(tracks, keys=['time', 'latitude', 'longitude'], headers=['Time', 'Latitude', 'Longitude'])
|
319
|
+
|
289
320
|
return response
|
290
321
|
|
291
322
|
|
292
|
-
def get_initialization_times():
|
323
|
+
def get_initialization_times(print_response=False):
|
293
324
|
"""
|
294
325
|
Get available WeatherMesh initialization times (also known as cycle times).
|
295
326
|
|
@@ -298,6 +329,12 @@ def get_initialization_times():
|
|
298
329
|
|
299
330
|
response = make_api_request(f"{FORECASTS_API_BASE_URL}/initialization_times.json")
|
300
331
|
|
332
|
+
if print_response:
|
333
|
+
print("Latest initialization time:", response['latest'])
|
334
|
+
print("Available initialization times:")
|
335
|
+
for time in response['available']:
|
336
|
+
print(f" - {time}")
|
337
|
+
|
301
338
|
return response
|
302
339
|
|
303
340
|
|
windborne/utils.py
CHANGED
@@ -46,7 +46,7 @@ def to_unix_timestamp(date_string):
|
|
46
46
|
|
47
47
|
# Supported date format
|
48
48
|
# Compact format YYYYMMDDHH
|
49
|
-
def parse_time(time, init_time_flag=None):
|
49
|
+
def parse_time(time, init_time_flag=None, require_past=False):
|
50
50
|
"""
|
51
51
|
Parse and validate initialization time with support for multiple formats.
|
52
52
|
Returns validated initialization time in ISO format, or None if invalid.
|
@@ -78,10 +78,9 @@ def parse_time(time, init_time_flag=None):
|
|
78
78
|
print(" - Initialization time hour must be 00, 06, 12, or 18")
|
79
79
|
exit(2)
|
80
80
|
|
81
|
-
if parsed_date > datetime.now():
|
82
|
-
print(f"
|
83
|
-
|
84
|
-
exit(1111)
|
81
|
+
if require_past and parsed_date > datetime.now():
|
82
|
+
print(f"Invalid date: {time} -- cannot be in the future")
|
83
|
+
exit(2)
|
85
84
|
|
86
85
|
return parsed_date.strftime('%Y-%m-%dT%H:00:00')
|
87
86
|
|
@@ -158,3 +157,33 @@ def save_arbitrary_response(output_file, response, csv_data_key=None):
|
|
158
157
|
else:
|
159
158
|
print("Unsupported file format. Please use either .json or .csv.")
|
160
159
|
exit(4)
|
160
|
+
|
161
|
+
|
162
|
+
def print_table(data, keys=None, headers=None):
|
163
|
+
if len(data) == 0:
|
164
|
+
print("No data found")
|
165
|
+
return
|
166
|
+
|
167
|
+
if keys is None:
|
168
|
+
keys = list(data[0].keys())
|
169
|
+
|
170
|
+
if headers is None:
|
171
|
+
headers = keys
|
172
|
+
|
173
|
+
# headers = ["Index", "Mission ID", "Mission Name"]
|
174
|
+
rows = [
|
175
|
+
[
|
176
|
+
str(value.get(key)) if key != 'i' else str(i)
|
177
|
+
for key in keys
|
178
|
+
]
|
179
|
+
for i, value in enumerate(data, start=1)
|
180
|
+
]
|
181
|
+
|
182
|
+
# Calculate column widths
|
183
|
+
col_widths = [max(len(cell) for cell in col) + 2 for col in zip(headers, *rows)]
|
184
|
+
|
185
|
+
# Display table
|
186
|
+
print("".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers))))
|
187
|
+
print("".join("-" * col_width for col_width in col_widths))
|
188
|
+
for row in rows:
|
189
|
+
print("".join(f"{row[i]:<{col_widths[i]}}" for i in range(len(row))))
|
@@ -0,0 +1,13 @@
|
|
1
|
+
windborne/__init__.py,sha256=0bPtPzBG3djZMVfyUNhapiEqCSyN8SSDsm_eCZ4kwhc,1783
|
2
|
+
windborne/api_request.py,sha256=zh1TaaZAaRfAXp2NYMja75fKeduWLfao02JRRFVpQCA,11108
|
3
|
+
windborne/cli.py,sha256=XvI9a9mO921a_LpI1ojN9Rn_x3sYq3XTkmUUT37wg4g,34835
|
4
|
+
windborne/cyclone_formatting.py,sha256=0S8S_PflRGm6ftUlyR2aGGyX6IRn0hbUTEWptu7f8Q8,7886
|
5
|
+
windborne/data_api.py,sha256=qVA0UGcnkBGYe8TFgqgFlFjZYCsS4ly1IL97tSpgKgI,29751
|
6
|
+
windborne/forecasts_api.py,sha256=QzdayDr3MaswsFibOdBoo6SCdRY86eE2E7kWqn-Jc1Y,15479
|
7
|
+
windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
|
8
|
+
windborne/utils.py,sha256=H8gvZ4Lrr0UmLl25iMZs6NsZliCY_73Ved_rBIqxJg4,7240
|
9
|
+
windborne-1.1.2.dist-info/METADATA,sha256=NR1lkRL5m-j3UoWVj_2ti14bSrIXZLtCbwS4DfUvko0,1235
|
10
|
+
windborne-1.1.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
11
|
+
windborne-1.1.2.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
12
|
+
windborne-1.1.2.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
13
|
+
windborne-1.1.2.dist-info/RECORD,,
|
windborne-1.1.1.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
windborne/__init__.py,sha256=0bPtPzBG3djZMVfyUNhapiEqCSyN8SSDsm_eCZ4kwhc,1783
|
2
|
-
windborne/api_request.py,sha256=J7kcCC3xiISHnrTLxMkFQCEu8rg8UntYvFfP5wkLabQ,11117
|
3
|
-
windborne/cli.py,sha256=hAnG1n6ZDICqj3nvjS8Xkc3psLeL-Hxewkmy-FABSCU,35807
|
4
|
-
windborne/cyclone_formatting.py,sha256=0S8S_PflRGm6ftUlyR2aGGyX6IRn0hbUTEWptu7f8Q8,7886
|
5
|
-
windborne/data_api.py,sha256=JBdedKWdx3eXKz0wGKqDLkR-QYl8O2SjutPL3LPx4l8,30457
|
6
|
-
windborne/forecasts_api.py,sha256=-IM78Dr10rV-CKVqRWXpptJPe7Hh64DNMO3P3zVgvuQ,13790
|
7
|
-
windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
|
8
|
-
windborne/utils.py,sha256=AJhvdwtfDrV0NQyN_I5JxXr6K3UgFlPCwUGqApe-c2E,6431
|
9
|
-
windborne-1.1.1.dist-info/METADATA,sha256=W5ZRFOnDBarbmqo8Wr4Ix8NWC47VqpsYaQYLVo3W5Vk,1235
|
10
|
-
windborne-1.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
11
|
-
windborne-1.1.1.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
12
|
-
windborne-1.1.1.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
13
|
-
windborne-1.1.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|