windborne 1.2.8__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.
- windborne/__init__.py +17 -34
- windborne/api_request.py +2 -0
- windborne/cli.py +112 -155
- windborne/data_api.py +46 -32
- windborne/forecasts_api.py +348 -156
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/METADATA +1 -1
- windborne-1.3.0.dist-info/RECORD +13 -0
- windborne-1.2.8.dist-info/RECORD +0 -13
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/WHEEL +0 -0
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/entry_points.txt +0 -0
- {windborne-1.2.8.dist-info → windborne-1.3.0.dist-info}/top_level.txt +0 -0
windborne/data_api.py
CHANGED
|
@@ -4,12 +4,12 @@ from datetime import datetime, timezone, timedelta
|
|
|
4
4
|
import csv
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
|
-
from .api_request import make_api_request
|
|
7
|
+
from .api_request import make_api_request, API_BASE_URL
|
|
8
8
|
from .observation_formatting import format_little_r, convert_to_netcdf
|
|
9
9
|
from .utils import to_unix_timestamp, save_arbitrary_response, print_table
|
|
10
10
|
from .track_formatting import save_track
|
|
11
11
|
|
|
12
|
-
DATA_API_BASE_URL = "
|
|
12
|
+
DATA_API_BASE_URL = f"{API_BASE_URL}/data/v1"
|
|
13
13
|
|
|
14
14
|
# ------------
|
|
15
15
|
# CORE RESOURCES
|
|
@@ -123,7 +123,7 @@ def get_super_observations_page(since=None, min_time=None, max_time=None, includ
|
|
|
123
123
|
return response
|
|
124
124
|
|
|
125
125
|
|
|
126
|
-
def save_observations_batch(observations, output_file, output_format, output_dir, start_time=None, end_time=None, bucket_hours=6.0, csv_headers=None, custom_save=None, prevent_overwrites=False):
|
|
126
|
+
def save_observations_batch(observations, output_file, output_format, output_dir, start_time=None, end_time=None, bucket_hours=6.0, csv_headers=None, custom_save=None, prevent_overwrites=False, verbose=True):
|
|
127
127
|
filtered_observations = observations
|
|
128
128
|
if start_time is not None:
|
|
129
129
|
filtered_observations = [obs for obs in observations if float(obs['timestamp']) >= start_time]
|
|
@@ -138,12 +138,12 @@ def save_observations_batch(observations, output_file, output_format, output_dir
|
|
|
138
138
|
if custom_save is not None:
|
|
139
139
|
custom_save(sorted_observations, output_file)
|
|
140
140
|
else:
|
|
141
|
-
save_observations_to_file(sorted_observations, output_file, csv_headers=csv_headers, prevent_overwrites=prevent_overwrites)
|
|
141
|
+
save_observations_to_file(sorted_observations, output_file, csv_headers=csv_headers, prevent_overwrites=prevent_overwrites, verbose=verbose)
|
|
142
142
|
else:
|
|
143
|
-
save_observations_batch_in_buckets(sorted_observations, output_format, output_dir, bucket_hours=bucket_hours, csv_headers=csv_headers, custom_save=custom_save, prevent_overwrites=prevent_overwrites)
|
|
143
|
+
save_observations_batch_in_buckets(sorted_observations, output_format, output_dir, bucket_hours=bucket_hours, csv_headers=csv_headers, custom_save=custom_save, prevent_overwrites=prevent_overwrites, verbose=verbose)
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
def save_observations_to_file(sorted_observations, output_file, csv_headers=None, prevent_overwrites=False):
|
|
146
|
+
def save_observations_to_file(sorted_observations, output_file, csv_headers=None, prevent_overwrites=False, verbose=True):
|
|
147
147
|
if len(sorted_observations) == 0:
|
|
148
148
|
print(f"Skipping empty file {output_file}")
|
|
149
149
|
return
|
|
@@ -169,10 +169,11 @@ def save_observations_to_file(sorted_observations, output_file, csv_headers=None
|
|
|
169
169
|
|
|
170
170
|
output_file = f"{base}.{i}.{ext}"
|
|
171
171
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
print("
|
|
175
|
-
|
|
172
|
+
if verbose:
|
|
173
|
+
print("-----------------------------------------------------\n")
|
|
174
|
+
print(f"Saving {len(sorted_observations)} {'observation' if len(sorted_observations) == 1 else 'observations'} to {output_file}")
|
|
175
|
+
if len(sorted_observations) > 10_000:
|
|
176
|
+
print("This may take a while...")
|
|
176
177
|
|
|
177
178
|
if output_file.endswith('.nc'):
|
|
178
179
|
first_obs_timestamp = float(sorted_observations[0]['timestamp'])
|
|
@@ -193,10 +194,11 @@ def save_observations_to_file(sorted_observations, output_file, csv_headers=None
|
|
|
193
194
|
with open(output_file, 'w') as file:
|
|
194
195
|
file.write('\n'.join(little_r_records))
|
|
195
196
|
|
|
196
|
-
|
|
197
|
+
if verbose:
|
|
198
|
+
print(f"Saved {len(sorted_observations)} {'observation' if len(sorted_observations) == 1 else 'observations'} to {output_file}")
|
|
197
199
|
|
|
198
200
|
|
|
199
|
-
def save_observations_batch_in_buckets(sorted_observations, output_format, output_dir, bucket_hours=6.0, csv_headers=None, custom_save=None, prevent_overwrites=False):
|
|
201
|
+
def save_observations_batch_in_buckets(sorted_observations, output_format, output_dir, bucket_hours=6.0, csv_headers=None, custom_save=None, prevent_overwrites=False, verbose=True):
|
|
200
202
|
if output_dir:
|
|
201
203
|
os.makedirs(output_dir, exist_ok=True)
|
|
202
204
|
print(f"Files will be saved to {output_dir}")
|
|
@@ -245,13 +247,13 @@ def save_observations_batch_in_buckets(sorted_observations, output_format, outpu
|
|
|
245
247
|
if custom_save is not None:
|
|
246
248
|
custom_save(segment, output_file)
|
|
247
249
|
else:
|
|
248
|
-
save_observations_to_file(segment, output_file, csv_headers=csv_headers, prevent_overwrites=prevent_overwrites)
|
|
250
|
+
save_observations_to_file(segment, output_file, csv_headers=csv_headers, prevent_overwrites=prevent_overwrites, verbose=verbose)
|
|
249
251
|
|
|
250
252
|
start_index = i
|
|
251
253
|
curtime += timedelta(hours=bucket_hours).seconds
|
|
252
254
|
|
|
253
255
|
|
|
254
|
-
def get_observations_core(api_args, csv_headers, get_page, start_time=None, end_time=None, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True):
|
|
256
|
+
def get_observations_core(api_args, csv_headers, get_page, start_time=None, end_time=None, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True, verbose=True):
|
|
255
257
|
"""
|
|
256
258
|
Fetches observations or superobservations between a start time and an optional end time and saves to files in specified format.
|
|
257
259
|
Files are broken up into time buckets, with filenames containing the time at the mid-point of the bucket.
|
|
@@ -308,7 +310,8 @@ def get_observations_core(api_args, csv_headers, get_page, start_time=None, end_
|
|
|
308
310
|
bucket_hours=bucket_hours,
|
|
309
311
|
csv_headers=csv_headers,
|
|
310
312
|
custom_save=custom_save,
|
|
311
|
-
prevent_overwrites=clear_batches
|
|
313
|
+
prevent_overwrites=clear_batches,
|
|
314
|
+
verbose=verbose
|
|
312
315
|
)
|
|
313
316
|
|
|
314
317
|
result = iterate_through_observations(get_page, api_args, callback=callback, batch_callback=save_with_context, exit_at_end=exit_at_end, clear_batches=clear_batches, batch_size=batch_size)
|
|
@@ -360,11 +363,12 @@ def iterate_through_observations(get_page, args, callback=None, batch_callback=N
|
|
|
360
363
|
if callback:
|
|
361
364
|
callback(response)
|
|
362
365
|
else:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
since_timestamp
|
|
366
|
-
|
|
367
|
-
|
|
366
|
+
if since is not None:
|
|
367
|
+
since_timestamp = since
|
|
368
|
+
if since_timestamp > 4_000_000_000: # in nanoseconds rather than seconds
|
|
369
|
+
since_timestamp /= 1_000_000_000
|
|
370
|
+
since_dt = datetime.fromtimestamp(since_timestamp, timezone.utc)
|
|
371
|
+
print(f"Fetched page with {len(observations)} observation(s) updated {since_dt} or later")
|
|
368
372
|
|
|
369
373
|
batched_observations.extend(observations)
|
|
370
374
|
|
|
@@ -407,7 +411,7 @@ def verify_observations_output_format(output_format):
|
|
|
407
411
|
|
|
408
412
|
exit(1)
|
|
409
413
|
|
|
410
|
-
def get_observations(start_time, end_time=None, include_updated_at=None, mission_id=None, min_latitude=None, max_latitude=None, min_longitude=None, max_longitude=None, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True):
|
|
414
|
+
def get_observations(start_time, end_time=None, include_updated_at=None, mission_id=None, min_latitude=None, max_latitude=None, min_longitude=None, max_longitude=None, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True, verbose=True):
|
|
411
415
|
"""
|
|
412
416
|
Fetches observations between a start time and an optional end time and saves to files in specified format.
|
|
413
417
|
Files are broken up into time buckets, with filenames containing the time at the mid-point of the bucket.
|
|
@@ -435,6 +439,7 @@ def get_observations(start_time, end_time=None, include_updated_at=None, mission
|
|
|
435
439
|
This allows custom processing or saving in custom formats.
|
|
436
440
|
custom_save (callable): Optional function to save observations in a custom format.
|
|
437
441
|
exit_at_end (bool): Whether to exit after fetching all observations or keep polling.
|
|
442
|
+
verbose (bool): Whether to print saving information.
|
|
438
443
|
"""
|
|
439
444
|
|
|
440
445
|
# Headers for CSV files
|
|
@@ -456,7 +461,7 @@ def get_observations(start_time, end_time=None, include_updated_at=None, mission
|
|
|
456
461
|
'include_mission_name': True
|
|
457
462
|
}
|
|
458
463
|
|
|
459
|
-
return get_observations_core(api_args, csv_headers, get_page=get_observations_page, start_time=start_time, end_time=end_time, output_file=output_file, bucket_hours=bucket_hours, output_format=output_format, output_dir=output_dir, callback=callback, custom_save=custom_save, exit_at_end=exit_at_end)
|
|
464
|
+
return get_observations_core(api_args, csv_headers, get_page=get_observations_page, start_time=start_time, end_time=end_time, output_file=output_file, bucket_hours=bucket_hours, output_format=output_format, output_dir=output_dir, callback=callback, custom_save=custom_save, exit_at_end=exit_at_end, verbose=verbose)
|
|
460
465
|
|
|
461
466
|
def poll_observations(**kwargs):
|
|
462
467
|
"""
|
|
@@ -475,7 +480,7 @@ def poll_observations(**kwargs):
|
|
|
475
480
|
|
|
476
481
|
get_observations(**kwargs, exit_at_end=False)
|
|
477
482
|
|
|
478
|
-
def get_super_observations(start_time, end_time=None, mission_id=None, include_updated_at=True, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True):
|
|
483
|
+
def get_super_observations(start_time, end_time=None, mission_id=None, include_updated_at=True, output_file=None, bucket_hours=6.0, output_format=None, output_dir=None, callback=None, custom_save=None, exit_at_end=True, verbose=True):
|
|
479
484
|
"""
|
|
480
485
|
Fetches super observations between a start time and an optional end time and saves to files in specified format.
|
|
481
486
|
Files are broken up into time buckets, with filenames containing the time at the mid-point of the bucket.
|
|
@@ -497,10 +502,12 @@ def get_super_observations(start_time, end_time=None, mission_id=None, include_u
|
|
|
497
502
|
This allows custom processing or saving in custom formats.
|
|
498
503
|
custom_save (callable): Optional function to save observations in a custom format.
|
|
499
504
|
exit_at_end (bool): Whether to exit after fetching all observations or keep polling.
|
|
505
|
+
verbose (bool): Whether to print saving information.
|
|
500
506
|
"""
|
|
501
507
|
csv_headers = [
|
|
502
508
|
"timestamp", "id", "time", "latitude", "longitude", "altitude", "humidity",
|
|
503
|
-
"mission_name", "pressure", "specific_humidity", "speed_u", "speed_v", "temperature"
|
|
509
|
+
"mission_name", "pressure", "specific_humidity", "speed_u", "speed_v", "temperature",
|
|
510
|
+
"mission_id", "updated_at"
|
|
504
511
|
]
|
|
505
512
|
|
|
506
513
|
api_args = {
|
|
@@ -512,7 +519,7 @@ def get_super_observations(start_time, end_time=None, mission_id=None, include_u
|
|
|
512
519
|
'include_mission_name': True
|
|
513
520
|
}
|
|
514
521
|
|
|
515
|
-
return get_observations_core(api_args, csv_headers, get_page=get_super_observations_page, start_time=start_time, end_time=end_time, output_file=output_file, bucket_hours=bucket_hours, output_format=output_format, output_dir=output_dir, callback=callback, custom_save=custom_save, exit_at_end=exit_at_end)
|
|
522
|
+
return get_observations_core(api_args, csv_headers, get_page=get_super_observations_page, start_time=start_time, end_time=end_time, output_file=output_file, bucket_hours=bucket_hours, output_format=output_format, output_dir=output_dir, callback=callback, custom_save=custom_save, exit_at_end=exit_at_end, verbose=verbose)
|
|
516
523
|
|
|
517
524
|
def poll_super_observations(**kwargs):
|
|
518
525
|
"""
|
|
@@ -556,7 +563,7 @@ def get_flying_missions(output_file=None, print_results=False):
|
|
|
556
563
|
'page_size': page_size
|
|
557
564
|
}
|
|
558
565
|
|
|
559
|
-
url = f"{DATA_API_BASE_URL}/
|
|
566
|
+
url = f"{DATA_API_BASE_URL}/flying_missions.json"
|
|
560
567
|
flying_missions_response = make_api_request(url, params=query_params)
|
|
561
568
|
|
|
562
569
|
flying_missions = flying_missions_response.get("missions", [])
|
|
@@ -700,21 +707,28 @@ def get_predicted_path(mission_id=None, output_file=None, print_result=False):
|
|
|
700
707
|
|
|
701
708
|
mission = get_flying_mission(mission_id)
|
|
702
709
|
|
|
703
|
-
url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/
|
|
710
|
+
url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/predicted_path.json"
|
|
704
711
|
response = make_api_request(url)
|
|
705
712
|
|
|
706
713
|
if response is None:
|
|
707
714
|
return
|
|
708
715
|
|
|
716
|
+
prediction = response.get('prediction') if isinstance(response, dict) else None
|
|
717
|
+
|
|
709
718
|
if output_file:
|
|
710
719
|
name = mission.get('name', mission_id)
|
|
711
|
-
|
|
720
|
+
# Save an empty list if prediction is not available to avoid crashes
|
|
721
|
+
save_track(output_file, {name: (prediction or [])}, time_key='time')
|
|
712
722
|
|
|
713
723
|
if print_result:
|
|
714
|
-
|
|
715
|
-
|
|
724
|
+
if not prediction:
|
|
725
|
+
print("Predicted flight path\n")
|
|
726
|
+
print("No predicted path available for this mission.")
|
|
727
|
+
else:
|
|
728
|
+
print("Predicted flight path\n")
|
|
729
|
+
print_table(prediction, keys=['time', 'latitude', 'longitude', 'altitude'], headers=['Time', 'Latitude', 'Longitude', 'Altitude'])
|
|
716
730
|
|
|
717
|
-
return
|
|
731
|
+
return prediction
|
|
718
732
|
|
|
719
733
|
|
|
720
734
|
def get_current_location(mission_id=None, output_file=None, print_result=False, verify_flying=True):
|
|
@@ -769,7 +783,7 @@ def get_flight_path(mission_id=None, output_file=None, print_result=False):
|
|
|
769
783
|
print("A mission id is required to get a flight path")
|
|
770
784
|
return
|
|
771
785
|
|
|
772
|
-
url = f"{DATA_API_BASE_URL}/missions/{mission_id}/
|
|
786
|
+
url = f"{DATA_API_BASE_URL}/missions/{mission_id}/flight_path.json"
|
|
773
787
|
response = make_api_request(url)
|
|
774
788
|
|
|
775
789
|
if response is None:
|