windborne 1.1.0__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 +16 -29
- windborne/data_api.py +44 -43
- windborne/forecasts_api.py +43 -6
- windborne/utils.py +34 -5
- {windborne-1.1.0.dist-info → windborne-1.1.2.dist-info}/METADATA +1 -1
- windborne-1.1.2.dist-info/RECORD +13 -0
- windborne-1.1.0.dist-info/RECORD +0 -13
- {windborne-1.1.0.dist-info → windborne-1.1.2.dist-info}/WHEEL +0 -0
- {windborne-1.1.0.dist-info → windborne-1.1.2.dist-info}/entry_points.txt +0 -0
- {windborne-1.1.0.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
|
@@ -365,12 +365,13 @@ def main():
|
|
365
365
|
)
|
366
366
|
|
367
367
|
elif args.command == 'flying-missions':
|
368
|
-
get_flying_missions(
|
368
|
+
get_flying_missions(output_file=args.output, print_results=(not args.output))
|
369
369
|
|
370
370
|
elif args.command == 'launch-site':
|
371
371
|
get_mission_launch_site(
|
372
372
|
mission_id=args.mission_id,
|
373
|
-
output_file=args.output
|
373
|
+
output_file=args.output,
|
374
|
+
print_result=(not args.output)
|
374
375
|
)
|
375
376
|
|
376
377
|
elif args.command == 'predict-path':
|
@@ -395,15 +396,12 @@ def main():
|
|
395
396
|
min_forecast_hour=min_forecast_hour,
|
396
397
|
max_forecast_hour=max_forecast_hour,
|
397
398
|
initialization_time=initialization_time,
|
398
|
-
output_file=args.output_file
|
399
|
+
output_file=args.output_file,
|
400
|
+
print_response=(not args.output_file)
|
399
401
|
)
|
400
402
|
|
401
403
|
elif args.command == 'init_times':
|
402
|
-
|
403
|
-
print("Available initialization times for point forecasts:\n")
|
404
|
-
pprint(get_initialization_times())
|
405
|
-
else:
|
406
|
-
print("We can't currently display available initialization times for point forecasts:\n")
|
404
|
+
get_initialization_times(print_response=True)
|
407
405
|
|
408
406
|
elif args.command == 'grid_temp_2m':
|
409
407
|
# Parse grid_temp_2m arguments
|
@@ -588,39 +586,28 @@ def main():
|
|
588
586
|
print("Too many arguments")
|
589
587
|
print("\nUsage: windborne hist_500hpa_wind_v initialization_time forecast_hour output_file")
|
590
588
|
|
591
|
-
elif args.command == '
|
589
|
+
elif args.command == 'tropical_cyclones':
|
592
590
|
# Parse cyclones arguments
|
593
|
-
basin_name = '
|
591
|
+
basin_name = 'all basins'
|
594
592
|
if args.basin:
|
595
593
|
basin_name = f"{args.basin} basin"
|
596
|
-
print(f"Checking for tropical cyclones only within {args.basin} basin\n")
|
597
594
|
|
598
595
|
if len(args.args) == 0:
|
599
|
-
|
600
|
-
|
601
|
-
print(f"Found {len(get_tropical_cyclones())} cyclone(s)\n")
|
602
|
-
pprint(get_tropical_cyclones(basin=args.basin))
|
603
|
-
return
|
604
|
-
else:
|
605
|
-
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
|
606
598
|
elif len(args.args) == 1:
|
607
599
|
if '.' in args.args[0]:
|
608
600
|
# Save tcs with the latest available initialization time in filename
|
609
601
|
get_tropical_cyclones(basin=args.basin, output_file=args.args[0])
|
610
602
|
else:
|
611
603
|
# Display tcs for selected initialization time
|
612
|
-
|
613
|
-
print(f"Loading tropical cyclones for initialization time {args.args[0]}\n")
|
614
|
-
print(f"Found {len(get_tropical_cyclones(initialization_time=args.args[0]))} cyclone(s)\n")
|
615
|
-
pprint(get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin))
|
616
|
-
else:
|
617
|
-
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)
|
618
605
|
elif len(args.args) == 2:
|
619
606
|
print(f"Saving tropical cyclones for initialization time {args.args[0]} and {basin_name}\n")
|
620
607
|
get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin, output_file=args.args[1])
|
621
608
|
else:
|
622
609
|
print("Error: Too many arguments")
|
623
|
-
print("Usage: windborne
|
610
|
+
print("Usage: windborne tropical_cyclones [initialization_time] output_file")
|
624
611
|
|
625
612
|
else:
|
626
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
|
|
@@ -257,6 +257,9 @@ def get_observations_core(api_args, csv_headers, get_page, start_time=None, end_
|
|
257
257
|
For example, for 6-hour buckets centered on 00 UTC, the start time should be 21 UTC of the previous day.
|
258
258
|
|
259
259
|
Args:
|
260
|
+
api_args (dict): Arguments to pass to the API endpoint.
|
261
|
+
csv_headers (list): Headers for CSV files.
|
262
|
+
get_page (callable): Function to fetch a page of observations.
|
260
263
|
start_time (str): A date string, supporting formats YYYY-MM-DD HH:MM:SS, YYYY-MM-DD_HH:MM and ISO strings,
|
261
264
|
representing the starting time of fetching data.
|
262
265
|
end_time (str): Optional. A date string, supporting formats YYYY-MM-DD HH:MM:SS, YYYY-MM-DD_HH:MM and ISO strings,
|
@@ -531,7 +534,7 @@ def poll_super_observations(**kwargs):
|
|
531
534
|
# ------------
|
532
535
|
# METADATA
|
533
536
|
# ------------
|
534
|
-
def get_flying_missions(
|
537
|
+
def get_flying_missions(output_file=None, print_results=False):
|
535
538
|
"""
|
536
539
|
Retrieves a list of currently flying missions.
|
537
540
|
In CLI mode, displays missions in a formatted table.
|
@@ -539,6 +542,7 @@ def get_flying_missions(from_cli=None, output_file=None):
|
|
539
542
|
Args:
|
540
543
|
output_file (str): Optional path to save the response data.
|
541
544
|
If provided, saves the data in CSV or JSON format.
|
545
|
+
print_results (bool): Whether to print the results in the CLI.
|
542
546
|
|
543
547
|
Returns:
|
544
548
|
dict: The API response containing list of flying missions.
|
@@ -549,35 +553,47 @@ def get_flying_missions(from_cli=None, output_file=None):
|
|
549
553
|
flying_missions = flying_missions_response.get("missions", [])
|
550
554
|
|
551
555
|
# Display currently flying missions only if we are in cli and we don't save info in file
|
552
|
-
if
|
553
|
-
|
554
|
-
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
556
|
+
if print_results:
|
557
|
+
if flying_missions:
|
558
|
+
print("Currently flying missions:\n")
|
559
|
+
|
560
|
+
# Define headers and data
|
561
|
+
headers = ["Index", "Mission ID", "Mission Name"]
|
562
|
+
rows = [
|
563
|
+
[str(i), mission.get("id", "N/A"), mission.get("name", "Unnamed Mission")]
|
564
|
+
for i, mission in enumerate(flying_missions, start=1)
|
565
|
+
]
|
566
|
+
|
567
|
+
# Kinda overkill | but it's a good practice if we ever change missions naming convention
|
568
|
+
# Calculate column widths
|
569
|
+
col_widths = [max(len(cell) for cell in col) + 2 for col in zip(headers, *rows)]
|
570
|
+
|
571
|
+
# Display table
|
572
|
+
print("".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers))))
|
573
|
+
print("".join("-" * col_width for col_width in col_widths))
|
574
|
+
for row in rows:
|
575
|
+
print("".join(f"{row[i]:<{col_widths[i]}}" for i in range(len(row))))
|
576
|
+
else:
|
577
|
+
print("No missions are currently flying.")
|
571
578
|
|
572
579
|
if output_file:
|
573
580
|
save_arbitrary_response(output_file, flying_missions_response, csv_data_key='missions')
|
574
581
|
|
575
|
-
return
|
582
|
+
return flying_missions
|
576
583
|
|
577
584
|
|
578
|
-
def get_mission_launch_site(mission_id=None, output_file=None):
|
585
|
+
def get_mission_launch_site(mission_id=None, output_file=None, print_result=False):
|
579
586
|
"""
|
580
587
|
Retrieves launch site information for a specified mission.
|
588
|
+
|
589
|
+
Args:
|
590
|
+
mission_id (str): The ID of the mission to fetch the launch site for.
|
591
|
+
output_file (str): Optional path to save the response data.
|
592
|
+
If provided, saves the data in CSV format.
|
593
|
+
print_result (bool): Whether to print the results in the CLI.
|
594
|
+
|
595
|
+
Returns:
|
596
|
+
dict: The API response containing the launch site information.
|
581
597
|
"""
|
582
598
|
if not mission_id:
|
583
599
|
print("Must provide mission ID")
|
@@ -586,7 +602,7 @@ def get_mission_launch_site(mission_id=None, output_file=None):
|
|
586
602
|
url = f"{DATA_API_BASE_URL}/missions/{mission_id}/launch_site.json"
|
587
603
|
response = make_api_request(url)
|
588
604
|
|
589
|
-
if response and
|
605
|
+
if response and print_result:
|
590
606
|
launch_site = response.get('launch_site')
|
591
607
|
if isinstance(launch_site, dict):
|
592
608
|
print("Mission launch site\n")
|
@@ -599,7 +615,7 @@ def get_mission_launch_site(mission_id=None, output_file=None):
|
|
599
615
|
if output_file:
|
600
616
|
save_arbitrary_response(output_file, response, csv_data_key='launch_site')
|
601
617
|
|
602
|
-
return response
|
618
|
+
return response.get('launch_site')
|
603
619
|
|
604
620
|
def get_predicted_path(mission_id=None, output_file=None):
|
605
621
|
"""
|
@@ -612,7 +628,7 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
612
628
|
If provided, saves the data in CSV format.
|
613
629
|
|
614
630
|
Returns:
|
615
|
-
|
631
|
+
list: The API response containing the predicted flight path data.
|
616
632
|
"""
|
617
633
|
if not mission_id:
|
618
634
|
print("To get the predicted flight path for a given mission you must provide a mission ID.")
|
@@ -629,22 +645,7 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
629
645
|
if flying_missions:
|
630
646
|
print("\nCurrently flying missions:\n")
|
631
647
|
|
632
|
-
|
633
|
-
headers = ["Index", "Mission ID", "Mission Name"]
|
634
|
-
rows = [
|
635
|
-
[str(i), mission.get("id", "N/A"), mission.get("name", "Unnamed Mission")]
|
636
|
-
for i, mission in enumerate(flying_missions, start=1)
|
637
|
-
]
|
638
|
-
|
639
|
-
# Kinda overkill | but it's a good practice if we ever change missions naming convention
|
640
|
-
# Calculate column widths
|
641
|
-
col_widths = [max(len(cell) for cell in col) + 2 for col in zip(headers, *rows)]
|
642
|
-
|
643
|
-
# Display table
|
644
|
-
print("".join(f"{headers[i]:<{col_widths[i]}}" for i in range(len(headers))))
|
645
|
-
print("".join("-" * col_width for col_width in col_widths))
|
646
|
-
for row in rows:
|
647
|
-
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'])
|
648
649
|
else:
|
649
650
|
print("No missions are currently flying.")
|
650
651
|
return
|
@@ -655,4 +656,4 @@ def get_predicted_path(mission_id=None, output_file=None):
|
|
655
656
|
if output_file:
|
656
657
|
save_arbitrary_response(output_file, response, csv_data_key='prediction')
|
657
658
|
|
658
|
-
return response
|
659
|
+
return response.get('prediction')
|
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.0.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=JqIjVXwZr_x9kuHYlXm45XYvl7NJBBckn-R69qne2Q8,35745
|
4
|
-
windborne/cyclone_formatting.py,sha256=0S8S_PflRGm6ftUlyR2aGGyX6IRn0hbUTEWptu7f8Q8,7886
|
5
|
-
windborne/data_api.py,sha256=rAcXUunz1lGOtxx59ID5Ki4thdGbtCl-aAw0DeVuouo,29645
|
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.0.dist-info/METADATA,sha256=2LBcuwXPOXfguHSqGFqQtg2GgGi6adNtjyENNUF3swQ,1235
|
10
|
-
windborne-1.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
11
|
-
windborne-1.1.0.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
|
12
|
-
windborne-1.1.0.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
|
13
|
-
windborne-1.1.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|