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 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
- return None
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
- cyclones_parser = subparsers.add_parser('cyclones', help='Get tropical cyclone forecasts')
214
- cyclones_parser.add_argument('-b', '--basin', help='Optional: filter tropical cyclones on basin[ NA, EP, WP, NI, SI, AU, SP]')
215
- cyclones_parser.add_argument('args', nargs='*',
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
- if get_initialization_times():
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 == 'cyclones':
589
+ elif args.command == 'tropical_cyclones':
593
590
  # Parse cyclones arguments
594
- basin_name = 'ALL basins'
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
- print("Loading tropical cyclones for our latest available initialization time\n")
601
- if get_tropical_cyclones(basin=args.basin):
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
- if get_tropical_cyclones(initialization_time=args.args[0], basin=args.basin):
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 cyclones [initialization_time] output_file")
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
- # Define headers and data
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
@@ -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
- print("Generating point forecast...")
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"How would it be to live in {parsed_date} ?\n")
83
- print("Looks like you are coming from the future!\n")
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))))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: windborne
3
- Version: 1.1.1
3
+ Version: 1.1.2
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=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,,
@@ -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,,