windborne 1.1.2__py3-none-any.whl → 1.1.3__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 CHANGED
@@ -14,6 +14,7 @@ from .data_api import (
14
14
  get_flying_missions,
15
15
  get_mission_launch_site,
16
16
  get_predicted_path,
17
+ get_flight_path
17
18
  )
18
19
 
19
20
  # Import Forecasts API functions
@@ -50,6 +51,7 @@ __all__ = [
50
51
  "get_flying_missions",
51
52
  "get_mission_launch_site",
52
53
  "get_predicted_path",
54
+ "get_flight_path",
53
55
 
54
56
  "get_point_forecasts",
55
57
  "get_initialization_times",
windborne/cli.py CHANGED
@@ -14,6 +14,7 @@ from . import (
14
14
  get_flying_missions,
15
15
  get_mission_launch_site,
16
16
  get_predicted_path,
17
+ get_flight_path,
17
18
 
18
19
  get_point_forecasts,
19
20
  get_initialization_times,
@@ -125,6 +126,11 @@ def main():
125
126
  prediction_parser.add_argument('mission_id', help='Mission ID')
126
127
  prediction_parser.add_argument('output', nargs='?', help='Output file')
127
128
 
129
+ # Get Flight Path Command
130
+ flight_path_parser = subparsers.add_parser('flight-path', help='Get traveled flight path')
131
+ flight_path_parser.add_argument('mission_id', help='Mission ID')
132
+ flight_path_parser.add_argument('output', nargs='?', help='Output file')
133
+
128
134
  ####################################################################################################################
129
135
  # FORECASTS API FUNCTIONS
130
136
  ####################################################################################################################
@@ -377,8 +383,18 @@ def main():
377
383
  elif args.command == 'predict-path':
378
384
  get_predicted_path(
379
385
  mission_id=args.mission_id,
380
- output_file=args.output
386
+ output_file=args.output,
387
+ print_result=(not args.output)
388
+ )
389
+
390
+ elif args.command == 'flight-path':
391
+ get_flight_path(
392
+ mission_id=args.mission_id,
393
+ output_file=args.output,
394
+ print_result=(not args.output)
381
395
  )
396
+
397
+
382
398
  ####################################################################################################################
383
399
  # FORECASTS API FUNCTIONS CALLED
384
400
  ####################################################################################################################
windborne/data_api.py CHANGED
@@ -7,6 +7,7 @@ import json
7
7
  from .api_request import make_api_request
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
+ from .track_formatting import save_track
10
11
 
11
12
  DATA_API_BASE_URL = "https://sensor-data.windbornesystems.com/api/v1"
12
13
 
@@ -551,6 +552,9 @@ def get_flying_missions(output_file=None, print_results=False):
551
552
  url = f"{DATA_API_BASE_URL}/missions.json"
552
553
  flying_missions_response = make_api_request(url)
553
554
  flying_missions = flying_missions_response.get("missions", [])
555
+ for mission in flying_missions:
556
+ if mission.get('number'):
557
+ mission['name'] = f"W-{mission['number']}"
554
558
 
555
559
  # Display currently flying missions only if we are in cli and we don't save info in file
556
560
  if print_results:
@@ -617,7 +621,8 @@ def get_mission_launch_site(mission_id=None, output_file=None, print_result=Fals
617
621
 
618
622
  return response.get('launch_site')
619
623
 
620
- def get_predicted_path(mission_id=None, output_file=None):
624
+
625
+ def get_predicted_path(mission_id=None, output_file=None, print_result=False):
621
626
  """
622
627
  Fetches the predicted flight path for a given mission.
623
628
  Displays currently flying missions if the provided mission ID is invalid.
@@ -625,7 +630,7 @@ def get_predicted_path(mission_id=None, output_file=None):
625
630
  Args:
626
631
  mission_id (str): The ID of the mission to fetch the prediction for.
627
632
  output_file (str): Optional path to save the response data.
628
- If provided, saves the data in CSV format.
633
+ print_result (bool): Whether to print the results in the CLI.
629
634
 
630
635
  Returns:
631
636
  list: The API response containing the predicted flight path data.
@@ -635,25 +640,70 @@ def get_predicted_path(mission_id=None, output_file=None):
635
640
  return
636
641
 
637
642
  # Check if provided mission ID belong to a flying mission
638
- flying_missions_response = get_flying_missions()
639
- flying_missions = flying_missions_response.get("missions", [])
643
+ flying_missions = get_flying_missions()
640
644
 
641
- if mission_id not in [mission.get("id") for mission in flying_missions]:
645
+ mission = None
646
+ for candidate in flying_missions:
647
+ if candidate.get('id') == mission_id or candidate.get('name') == mission_id:
648
+ mission = candidate
649
+ break
650
+
651
+ if mission is None:
642
652
  print(f"Provided mission ID '{mission_id}' does not belong to a mission that is currently flying.")
643
653
 
644
654
  # Display currently flying missions
645
655
  if flying_missions:
646
656
  print("\nCurrently flying missions:\n")
647
657
 
648
- print_table(flying_missions, keys=['i', 'id', 'name'], headers=['Index', 'Mission ID', 'Mission Name'])
658
+ print_table(flying_missions, keys=['id', 'name'], headers=['Mission ID', 'Mission Name'])
649
659
  else:
650
660
  print("No missions are currently flying.")
651
661
  return
652
662
 
653
- url = f"{DATA_API_BASE_URL}/missions/{mission_id}/prediction.json"
663
+ url = f"{DATA_API_BASE_URL}/missions/{mission.get('id')}/prediction.json"
654
664
  response = make_api_request(url)
655
665
 
666
+ if response is None:
667
+ return
668
+
656
669
  if output_file:
657
- save_arbitrary_response(output_file, response, csv_data_key='prediction')
658
-
659
- return response.get('prediction')
670
+ name = mission.get('name', mission_id)
671
+ save_track(output_file, {name: response['prediction']}, time_key='time')
672
+
673
+ if print_result:
674
+ print("Predicted flight path\n")
675
+ print_table(response['prediction'], keys=['time', 'latitude', 'longitude', 'altitude'], headers=['Time', 'Latitude', 'Longitude', 'Altitude'])
676
+
677
+ return response.get('prediction')
678
+
679
+
680
+ def get_flight_path(mission_id=None, output_file=None, print_result=False):
681
+ """
682
+ Fetches the flight path for a given mission.
683
+
684
+ Args:
685
+ mission_id (str): The ID of the mission to fetch the flight path for.
686
+ output_file (str): Optional path to save the response data.
687
+ print_result (bool): Whether to print the results in the CLI.
688
+
689
+ Returns:
690
+ list: The API response containing the flight path.
691
+ """
692
+ if not mission_id:
693
+ print("A mission id is required to get a flight path")
694
+ return
695
+
696
+ url = f"{DATA_API_BASE_URL}/missions/{mission_id}/flight_data.json"
697
+ response = make_api_request(url)
698
+
699
+ if response is None:
700
+ return
701
+
702
+ if output_file:
703
+ save_track(output_file, {mission_id: response['flight_data']}, time_key='transmit_time')
704
+
705
+ if print_result:
706
+ print("Flight path\n")
707
+ print_table(response['flight_data'], keys=['transmit_time', 'latitude', 'longitude', 'altitude'], headers=['Time', 'Latitude', 'Longitude', 'Altitude'])
708
+
709
+ return response.get('flight_data')
@@ -1,5 +1,4 @@
1
1
  import requests
2
- import json
3
2
 
4
3
  from .utils import (
5
4
  parse_time,
@@ -7,22 +6,14 @@ from .utils import (
7
6
  print_table
8
7
  )
9
8
 
10
- from .api_request import (
11
- make_api_request
12
- )
13
-
14
- from .cyclone_formatting import (
15
- save_track_as_geojson,
16
- save_track_as_gpx,
17
- save_track_as_kml,
18
- save_track_as_little_r
19
- )
9
+ from .api_request import make_api_request
10
+ from .track_formatting import save_track
20
11
 
21
12
  FORECASTS_API_BASE_URL = "https://forecasts.windbornesystems.com/api/v1"
22
13
  FORECASTS_GRIDDED_URL = f"{FORECASTS_API_BASE_URL}/gridded"
23
14
  FORECASTS_HISTORICAL_URL = f"{FORECASTS_API_BASE_URL}/gridded/historical"
24
15
  FORECASTS_TCS_URL = f"{FORECASTS_API_BASE_URL}/tropical_cyclones"
25
- TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r')
16
+ TCS_SUPPORTED_FORMATS = ('.csv', '.json', '.geojson', '.gpx', '.kml', '.little_r')
26
17
 
27
18
 
28
19
  # Point forecasts
@@ -263,11 +254,7 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
263
254
  response = make_api_request(FORECASTS_TCS_URL, params=params)
264
255
 
265
256
  if output_file:
266
- if '.' not in output_file:
267
- print("You have to provide a filetype for your output file.")
268
- print_tc_supported_formats()
269
- exit (4)
270
- elif not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
257
+ if not output_file.lower().endswith(TCS_SUPPORTED_FORMATS):
271
258
  print("Unsupported file format.")
272
259
  print_tc_supported_formats()
273
260
  exit(44)
@@ -276,37 +263,14 @@ def get_tropical_cyclones(initialization_time=None, basin=None, output_file=None
276
263
  # make_api_request covers 403, 404, 502, HTTP, Connections Errors
277
264
  # If we pass all of these and we get an empty dictionary ==> there are no active TCs
278
265
  print("There are no active tropical cyclones for your request\n")
279
- print("We didn't save any file on your machine.")
280
266
  # It's pointless to save an empty file
281
267
  # save_response_to_file() will throw error on saving {}
282
268
  elif response is None:
283
269
  print("-------------------------------------------------------")
284
270
  print("You are too quick!\nThe tropical cyclone data for initialization time are not uploaded yet.")
285
- print('You may check again in a few hours again.')
286
- elif output_file.lower().endswith('.csv'):
287
- # Flatten for CSV
288
- flattened_data = []
289
- for cyclone_id, tracks in response.items():
290
- for track in tracks:
291
- track_data = {
292
- 'cyclone_id': cyclone_id,
293
- 'latitude': track['latitude'],
294
- 'longitude': track['longitude'],
295
- 'time': track['time']
296
- }
297
- flattened_data.append(track_data)
298
- save_arbitrary_response(output_file, {'prediction': flattened_data}, csv_data_key='prediction')
299
- elif output_file.lower().endswith('.json'):
300
- # Direct save for JSON
301
- save_arbitrary_response(output_file, response)
302
- elif output_file.lower().endswith('.geojson'):
303
- save_track_as_geojson(output_file, response)
304
- elif output_file.lower().endswith('.gpx'):
305
- save_track_as_gpx(output_file, response)
306
- elif output_file.lower().endswith('.kml'):
307
- save_track_as_kml(output_file, response)
308
- elif output_file.lower().endswith('.little_r'):
309
- save_track_as_little_r(output_file, response)
271
+ print('You may check again in a few hour.')
272
+ else:
273
+ save_track(output_file, response, require_ids=True)
310
274
 
311
275
  if print_response:
312
276
  if len(response) == 0:
@@ -1,15 +1,69 @@
1
- from datetime import datetime, timezone
1
+ from datetime import datetime
2
+ import json
2
3
 
4
+ TRACK_SUPPORTED_FORMATS = ['.csv', '.json', '.geojson', '.gpx', '.kml', 'little_r']
3
5
 
4
- def save_track_as_little_r(filename, cyclone_data):
6
+ def save_track(output_file, track_data, time_key='time', require_ids=False):
5
7
  """
6
- Convert and save cyclone data in little_R format.
8
+ Save track data to a file in the specified format.
9
+ Expects track_data to be a dictionary with cyclone/mission IDs as keys and lists of track points as values.
7
10
  """
11
+ include_id = require_ids or len(track_data) > 1
12
+
13
+ if output_file.lower().endswith('.json'):
14
+ with open(output_file, 'w', encoding='utf-8') as f:
15
+ json.dump(track_data, f, indent=4)
16
+ elif output_file.lower().endswith('.csv'):
17
+ save_track_as_csv(output_file, track_data, time_key=time_key, include_id=include_id)
18
+ elif output_file.lower().endswith('.geojson'):
19
+ save_track_as_geojson(output_file, track_data, time_key=time_key)
20
+ elif output_file.lower().endswith('.gpx'):
21
+ save_track_as_gpx(output_file, track_data, time_key=time_key)
22
+ elif output_file.lower().endswith('.kml'):
23
+ save_track_as_kml(output_file, track_data)
24
+ elif output_file.lower().endswith('.little_r'):
25
+ save_track_as_little_r(output_file, track_data, time_key=time_key)
26
+ else:
27
+ print(f"Unsupported file format. Supported formats are: {', '.join(TRACK_SUPPORTED_FORMATS)}")
28
+ return
29
+
30
+
31
+ def save_track_as_csv(filename, track_data, time_key='time', include_id=False):
32
+ """
33
+ Convert and save track data as CSV.
34
+ """
35
+ flattened_data = []
36
+ for name, tracks in track_data.items():
37
+ for track in tracks:
38
+ track_data = {
39
+ 'id': name,
40
+ 'latitude': track['latitude'],
41
+ 'longitude': track['longitude'],
42
+ 'time': track[time_key]
43
+ }
44
+ flattened_data.append(track_data)
45
+
8
46
  with open(filename, 'w', encoding='utf-8') as f:
9
- for cyclone_id, tracks in cyclone_data.items():
47
+ if include_id:
48
+ f.write('id,latitude,longitude,time\n')
49
+ else:
50
+ f.write('latitude,longitude,time\n')
51
+
52
+ for row in flattened_data:
53
+ if include_id:
54
+ f.write(f"{row['id']},{row['latitude']},{row['longitude']},{row['time']}\n")
55
+ else:
56
+ f.write(f"{row['latitude']},{row['longitude']},{row['time']}\n")
57
+
58
+ def save_track_as_little_r(filename, track_data, time_key='time'):
59
+ """
60
+ Convert and save track data in little_R format.
61
+ """
62
+ with open(filename, 'w', encoding='utf-8') as f:
63
+ for cyclone_id, tracks in track_data.items():
10
64
  for track in tracks:
11
65
  # Parse the time
12
- dt = datetime.fromisoformat(track['time'].replace('Z', '+00:00'))
66
+ dt = datetime.fromisoformat(track[time_key].replace('Z', '+00:00'))
13
67
 
14
68
  # Header line 1
15
69
  header1 = f"{float(track['latitude']):20.5f}{float(track['longitude']):20.5f}{'HMS':40}"
@@ -35,21 +89,20 @@ def save_track_as_little_r(filename, cyclone_data):
35
89
  print("Saved to", filename)
36
90
 
37
91
 
38
- def save_track_as_kml(filename, cyclone_data):
92
+ def save_track_as_kml(filename, track_data):
39
93
  """
40
- Convert and save cyclone data as KML, handling meridian crossing.
94
+ Convert and save track data as KML, handling meridian crossing.
41
95
  """
42
96
  kml = '<?xml version="1.0" encoding="UTF-8"?>\n'
43
97
  kml += '<kml xmlns="http://www.opengis.net/kml/2.2">\n<Document>\n'
44
98
 
45
- for cyclone_id, tracks in cyclone_data.items():
46
- kml += f' <Placemark>\n <name>{cyclone_id}</name>\n <MultiGeometry>\n'
99
+ for id, tracks in track_data.items():
100
+ kml += f' <Placemark>\n <name>{id}</name>\n <MultiGeometry>\n'
47
101
 
48
102
  current_segment = []
49
103
 
50
104
  for i in range(len(tracks)):
51
105
  lon = float(tracks[i]['longitude'])
52
- lat = float(tracks[i]['latitude'])
53
106
 
54
107
  if not current_segment:
55
108
  current_segment.append(tracks[i])
@@ -61,7 +114,7 @@ def save_track_as_kml(filename, cyclone_data):
61
114
  if abs(lon - prev_lon) > 180:
62
115
  # Write the current segment
63
116
  kml += ' <LineString>\n <coordinates>\n'
64
- coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
117
+ coordinates = [f' {track["longitude"]},{track["latitude"]},{track.get("altitude", 0)}'
65
118
  for track in current_segment]
66
119
  kml += '\n'.join(coordinates)
67
120
  kml += '\n </coordinates>\n </LineString>\n'
@@ -74,7 +127,7 @@ def save_track_as_kml(filename, cyclone_data):
74
127
  # Write the last segment if it's not empty
75
128
  if current_segment:
76
129
  kml += ' <LineString>\n <coordinates>\n'
77
- coordinates = [f' {track["longitude"]},{track["latitude"]},{0}'
130
+ coordinates = [f' {track["longitude"]},{track["latitude"]},{track.get("altitude", 0)}'
78
131
  for track in current_segment]
79
132
  kml += '\n'.join(coordinates)
80
133
  kml += '\n </coordinates>\n </LineString>\n'
@@ -88,12 +141,12 @@ def save_track_as_kml(filename, cyclone_data):
88
141
  print(f"Saved to {filename}")
89
142
 
90
143
 
91
- def save_track_as_gpx(filename, cyclone_data):
92
- """Convert and save cyclone data as GPX, handling meridian crossing."""
144
+ def save_track_as_gpx(filename, track_data, time_key='time'):
145
+ """Convert and save track data as GPX, handling meridian crossing."""
93
146
  gpx = '<?xml version="1.0" encoding="UTF-8"?>\n'
94
147
  gpx += '<gpx version="1.1" creator="Windborne" xmlns="http://www.topografix.com/GPX/1/1">\n'
95
148
 
96
- for cyclone_id, tracks in cyclone_data.items():
149
+ for cyclone_id, tracks in track_data.items():
97
150
  gpx += f' <trk>\n <name>{cyclone_id}</name>\n'
98
151
 
99
152
  current_segment = []
@@ -101,7 +154,6 @@ def save_track_as_gpx(filename, cyclone_data):
101
154
 
102
155
  for i in range(len(tracks)):
103
156
  lon = float(tracks[i]['longitude'])
104
- lat = float(tracks[i]['latitude'])
105
157
 
106
158
  if not current_segment:
107
159
  current_segment.append(tracks[i])
@@ -130,7 +182,7 @@ def save_track_as_gpx(filename, cyclone_data):
130
182
  gpx += ' <trkseg>\n'
131
183
  for point in current_segment:
132
184
  gpx += f' <trkpt lat="{point["latitude"]}" lon="{point["longitude"]}">\n'
133
- gpx += f' <time>{point["time"]}</time>\n'
185
+ gpx += f' <time>{point[time_key]}</time>\n'
134
186
  gpx += ' </trkpt>\n'
135
187
  gpx += ' </trkseg>\n'
136
188
 
@@ -143,10 +195,10 @@ def save_track_as_gpx(filename, cyclone_data):
143
195
  print(f"Saved to {filename}")
144
196
 
145
197
 
146
- def save_track_as_geojson(filename, cyclone_data):
147
- """Convert and save cyclone data as GeoJSON, handling meridian crossing."""
198
+ def save_track_as_geojson(filename, track_data, time_key='time'):
199
+ """Convert and save track data as GeoJSON, handling meridian crossing."""
148
200
  features = []
149
- for cyclone_id, tracks in cyclone_data.items():
201
+ for id, tracks in track_data.items():
150
202
  # Initialize lists to store line segments
151
203
  line_segments = []
152
204
  current_segment = []
@@ -188,9 +240,9 @@ def save_track_as_geojson(filename, cyclone_data):
188
240
  feature = {
189
241
  "type": "Feature",
190
242
  "properties": {
191
- "cyclone_id": cyclone_id,
192
- "start_time": tracks[0]['time'],
193
- "end_time": tracks[-1]['time']
243
+ "id": id,
244
+ "start_time": tracks[0][time_key],
245
+ "end_time": tracks[-1][time_key]
194
246
  },
195
247
  "geometry": {
196
248
  "type": "MultiLineString",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: windborne
3
- Version: 1.1.2
3
+ Version: 1.1.3
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=DcxmiLq4UnjoRjZd4UeuYt29FBsi9paMXGtI91Itn04,1826
2
+ windborne/api_request.py,sha256=zh1TaaZAaRfAXp2NYMja75fKeduWLfao02JRRFVpQCA,11108
3
+ windborne/cli.py,sha256=xRPRLh50iC4Uu0AYEHL-LHRxUnpxqB_Mn-easp5VSK4,35370
4
+ windborne/data_api.py,sha256=QsG1NJyOCzTrTLRaoz5Z4nGRI0HCMEJvbDyRUlY3Usg,31388
5
+ windborne/forecasts_api.py,sha256=E0X9FyUW2yz3pQZKkFBYDMeg6PhFJ1bp4z1Cg1g0uGs,13987
6
+ windborne/observation_formatting.py,sha256=c739aaun6aaYhXl5VI-SRGR-TDS355_0Bfu1t6McoiM,14993
7
+ windborne/track_formatting.py,sha256=LaLfTyjpWoOtHmReJPLViY0MKm_iPL_5I2OB_lNvGGA,10054
8
+ windborne/utils.py,sha256=H8gvZ4Lrr0UmLl25iMZs6NsZliCY_73Ved_rBIqxJg4,7240
9
+ windborne-1.1.3.dist-info/METADATA,sha256=unPmA3uPzt8obgqSmJSrsSzSLat4yH7o3uJtb-i3wy0,1235
10
+ windborne-1.1.3.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
11
+ windborne-1.1.3.dist-info/entry_points.txt,sha256=j_YrqdCDrCd7p5MIwQ2BYwNXEi95VNANzLRJmcXEg1U,49
12
+ windborne-1.1.3.dist-info/top_level.txt,sha256=PE9Lauriu5S5REf7JKhXprufZ_V5RiZ_TnfnrLGJrmE,10
13
+ windborne-1.1.3.dist-info/RECORD,,
@@ -1,13 +0,0 @@
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,,