windborne 1.2.3__tar.gz → 1.2.5__tar.gz
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-1.2.3 → windborne-1.2.5}/PKG-INFO +3 -1
- {windborne-1.2.3 → windborne-1.2.5}/README.md +2 -0
- {windborne-1.2.3 → windborne-1.2.5}/pyproject.toml +1 -1
- {windborne-1.2.3 → windborne-1.2.5}/windborne/__init__.py +6 -2
- windborne-1.2.5/windborne/api_request.py +187 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne/cli.py +21 -1
- {windborne-1.2.3 → windborne-1.2.5}/windborne/forecasts_api.py +38 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/PKG-INFO +3 -1
- windborne-1.2.3/windborne/api_request.py +0 -229
- {windborne-1.2.3 → windborne-1.2.5}/setup.cfg +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne/data_api.py +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne/observation_formatting.py +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne/track_formatting.py +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne/utils.py +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/SOURCES.txt +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/dependency_links.txt +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/entry_points.txt +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/requires.txt +0 -0
- {windborne-1.2.3 → windborne-1.2.5}/windborne.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: windborne
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.5
|
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
|
@@ -29,3 +29,5 @@ Run `windborne --help` for more information.
|
|
29
29
|
|
30
30
|
## Further information and help request
|
31
31
|
If you encounter issues or have questions, please ask your WindBorne Systems contact or email data@windbornesystems.com.
|
32
|
+
|
33
|
+
For development of this package, see [README_dev.md](README_dev.md)
|
@@ -13,3 +13,5 @@ Run `windborne --help` for more information.
|
|
13
13
|
|
14
14
|
## Further information and help request
|
15
15
|
If you encounter issues or have questions, please ask your WindBorne Systems contact or email data@windbornesystems.com.
|
16
|
+
|
17
|
+
For development of this package, see [README_dev.md](README_dev.md)
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "windborne"
|
7
|
-
version = "1.2.
|
7
|
+
version = "1.2.5"
|
8
8
|
description = "A Python library for interacting with WindBorne Data and Forecasts API"
|
9
9
|
readme = {file = "README.md", content-type = "text/markdown"}
|
10
10
|
authors = [
|
@@ -39,7 +39,9 @@ from .forecasts_api import (
|
|
39
39
|
get_historical_500hpa_geopotential,
|
40
40
|
get_historical_500hpa_wind_u, get_historical_500hpa_wind_v,
|
41
41
|
|
42
|
-
get_tropical_cyclones
|
42
|
+
get_tropical_cyclones,
|
43
|
+
|
44
|
+
get_population_weighted_hdd
|
43
45
|
)
|
44
46
|
|
45
47
|
# Define what should be available when users import *
|
@@ -82,5 +84,7 @@ __all__ = [
|
|
82
84
|
"get_historical_500hpa_geopotential",
|
83
85
|
"get_historical_500hpa_wind_u",
|
84
86
|
"get_historical_500hpa_wind_v",
|
85
|
-
"get_tropical_cyclones"
|
87
|
+
"get_tropical_cyclones",
|
88
|
+
|
89
|
+
"get_population_weighted_hdd"
|
86
90
|
]
|
@@ -0,0 +1,187 @@
|
|
1
|
+
import jwt
|
2
|
+
import time
|
3
|
+
import requests
|
4
|
+
import re
|
5
|
+
import os
|
6
|
+
|
7
|
+
def is_valid_uuid_v4(client_id):
|
8
|
+
return re.fullmatch(r"[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}", client_id) is not None
|
9
|
+
|
10
|
+
|
11
|
+
def is_valid_client_id_format(client_id):
|
12
|
+
return re.fullmatch(r"[a-z0-9_]+", client_id) is not None
|
13
|
+
|
14
|
+
|
15
|
+
def get_api_credentials():
|
16
|
+
client_id = os.getenv('WB_CLIENT_ID')
|
17
|
+
api_key = os.getenv('WB_API_KEY')
|
18
|
+
|
19
|
+
return client_id, api_key
|
20
|
+
|
21
|
+
|
22
|
+
def verify_api_credentials(client_id, api_key):
|
23
|
+
if not client_id and not api_key:
|
24
|
+
raise ValueError(
|
25
|
+
"To access the WindBorne API, set your Client ID and API key by setting the environment variables WB_CLIENT_ID and WB_API_KEY. "
|
26
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction. "
|
27
|
+
"To get an API key, email data@windbornesystems.com."
|
28
|
+
)
|
29
|
+
|
30
|
+
if not client_id:
|
31
|
+
raise ValueError(
|
32
|
+
"To access the WindBorne API, you need to set your Client ID by setting the environment variable WB_CLIENT_ID. "
|
33
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction. "
|
34
|
+
"To get an API key, email data@windbornesystems.com."
|
35
|
+
)
|
36
|
+
|
37
|
+
if not api_key:
|
38
|
+
raise ValueError(
|
39
|
+
"To access the WindBorne API, you need to set your API key by setting the environment variable WB_API_KEY. "
|
40
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction. "
|
41
|
+
"To get an API key, email data@windbornesystems.com."
|
42
|
+
)
|
43
|
+
|
44
|
+
if len(client_id) in [32, 35] and len(api_key) not in [32, 35]:
|
45
|
+
raise ValueError(
|
46
|
+
f"Your Client ID and API Key are likely swapped. Current Client ID: {client_id}, Current API Key: {api_key}. "
|
47
|
+
"Swap them or modify them accordingly to get access to WindBorne API. "
|
48
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction."
|
49
|
+
)
|
50
|
+
|
51
|
+
# Validate WB_CLIENT_ID format
|
52
|
+
if is_valid_uuid_v4(client_id):
|
53
|
+
raise NotImplementedError(
|
54
|
+
"Personal API tokens are not yet supported. "
|
55
|
+
"You will need to get a globally-authorizing API key. "
|
56
|
+
"For questions, email data@windbornesystems.com."
|
57
|
+
)
|
58
|
+
|
59
|
+
if not (is_valid_uuid_v4(client_id) or is_valid_client_id_format(client_id)):
|
60
|
+
raise ValueError(
|
61
|
+
f"Your Client ID is misformatted: {client_id}. "
|
62
|
+
"It should either be a valid UUID v4 or consist of only lowercase letters, digits, and underscores ([a-z0-9_]). "
|
63
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction."
|
64
|
+
)
|
65
|
+
|
66
|
+
# Validate WB_API_KEY for both newer and older formats
|
67
|
+
if api_key.startswith("wb_"):
|
68
|
+
if len(api_key) != 35:
|
69
|
+
raise ValueError(
|
70
|
+
f"Your API key is misformatted: {api_key}. "
|
71
|
+
"API keys starting with 'wb_' must be 35 characters long (including the 'wb_' prefix). "
|
72
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction."
|
73
|
+
)
|
74
|
+
elif len(api_key) != 32: # For early tokens
|
75
|
+
raise ValueError(
|
76
|
+
f"Your API key is misformatted: {api_key}. "
|
77
|
+
"API keys created in 2023 or earlier must be exactly 32 characters long. "
|
78
|
+
"For instructions, refer to https://windbornesystems.com/docs/api/cli#introduction or https://windbornesystems.com/docs/api/pip_data#introduction."
|
79
|
+
)
|
80
|
+
|
81
|
+
|
82
|
+
VERIFIED_WB_CLIENT_ID = None
|
83
|
+
VERIFIED_WB_API_KEY = None
|
84
|
+
|
85
|
+
def get_verified_api_credentials():
|
86
|
+
global VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY
|
87
|
+
|
88
|
+
if VERIFIED_WB_CLIENT_ID is None or VERIFIED_WB_API_KEY is None:
|
89
|
+
VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY = get_api_credentials()
|
90
|
+
verify_api_credentials(VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY)
|
91
|
+
|
92
|
+
return VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY
|
93
|
+
|
94
|
+
|
95
|
+
def make_api_request(url, params=None, as_json=True, retry_counter=0):
|
96
|
+
"""
|
97
|
+
Make an authenticated request to the WindBorne API.
|
98
|
+
|
99
|
+
This uses a JWT under the hood
|
100
|
+
While basic auth is supported, this method reduces the odds of an improper configuration accidentally leaking the keys
|
101
|
+
|
102
|
+
:param url: The URL to make the request to
|
103
|
+
:param params: The parameters to pass to the request
|
104
|
+
:param as_json: Whether to return the response as JSON or as a requests.Response object
|
105
|
+
:param retry_counter: The number of times the request has been retried
|
106
|
+
:return:
|
107
|
+
"""
|
108
|
+
if retry_counter >= 5:
|
109
|
+
raise ConnectionError("Max retries to API reached.")
|
110
|
+
|
111
|
+
client_id, api_key = get_verified_api_credentials()
|
112
|
+
|
113
|
+
if is_valid_uuid_v4(client_id):
|
114
|
+
token_id = client_id
|
115
|
+
client_id = 'api_token'
|
116
|
+
|
117
|
+
signed_token = jwt.encode({
|
118
|
+
'client_id': client_id,
|
119
|
+
'iat': int(time.time()),
|
120
|
+
'token_id': token_id
|
121
|
+
}, api_key, algorithm='HS256')
|
122
|
+
else:
|
123
|
+
signed_token = jwt.encode({
|
124
|
+
'client_id': client_id,
|
125
|
+
'iat': int(time.time()),
|
126
|
+
}, api_key, algorithm='HS256')
|
127
|
+
|
128
|
+
try:
|
129
|
+
if params:
|
130
|
+
response = requests.get(url, auth=(client_id, signed_token), params=params)
|
131
|
+
else:
|
132
|
+
response = requests.get(url, auth=(client_id, signed_token))
|
133
|
+
|
134
|
+
response.raise_for_status()
|
135
|
+
|
136
|
+
if as_json:
|
137
|
+
return response.json()
|
138
|
+
else:
|
139
|
+
return response
|
140
|
+
|
141
|
+
except requests.exceptions.HTTPError as http_err:
|
142
|
+
if http_err.response.status_code == 403:
|
143
|
+
print("--------------------------------------")
|
144
|
+
print("We couldn't authenticate your request.")
|
145
|
+
print("--------------------------------------")
|
146
|
+
print("You likely don't have permission to access this resource.\n")
|
147
|
+
print("For questions, email data@windbornesystems.com.")
|
148
|
+
elif http_err.response.status_code in [404, 400]:
|
149
|
+
print("-------------------------------------------------------")
|
150
|
+
print("Our server couldn't find the information you requested.")
|
151
|
+
print("-------------------------------------------------------")
|
152
|
+
print(f"URL: {url}")
|
153
|
+
print(f"Error: {http_err.response.status_code}")
|
154
|
+
print("-------------------------------------------------------")
|
155
|
+
if params:
|
156
|
+
print("\nParameters provided:")
|
157
|
+
for key, value in params.items():
|
158
|
+
print(f" {key}: {value}")
|
159
|
+
else:
|
160
|
+
if 'missions/' in url:
|
161
|
+
mission_id = url.split('/missions/')[1].split('/')[0]
|
162
|
+
print(f"Mission ID provided: {mission_id}")
|
163
|
+
print(f"No mission found with id: {mission_id}")
|
164
|
+
print("-------------------------------------------------------")
|
165
|
+
print("Response text:")
|
166
|
+
print(http_err.response.text)
|
167
|
+
return None
|
168
|
+
elif http_err.response.status_code == 502:
|
169
|
+
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
170
|
+
print(f"Underlying error: 502 Bad Gateway")
|
171
|
+
time.sleep(2**retry_counter)
|
172
|
+
return make_api_request(url, params, as_json, retry_counter + 1)
|
173
|
+
else:
|
174
|
+
# Re-raise the HTTP error instead of exiting
|
175
|
+
raise http_err
|
176
|
+
except requests.exceptions.ConnectionError as conn_err:
|
177
|
+
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
178
|
+
print(f"Underlying error: \n\n{conn_err}")
|
179
|
+
time.sleep(2**retry_counter)
|
180
|
+
return make_api_request(url, params, as_json, retry_counter + 1)
|
181
|
+
except requests.exceptions.Timeout as timeout_err:
|
182
|
+
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
183
|
+
print(f"Underlying error: \n\n{timeout_err}")
|
184
|
+
time.sleep(2**retry_counter)
|
185
|
+
return make_api_request(url, params, as_json, retry_counter + 1)
|
186
|
+
except requests.exceptions.RequestException as req_err:
|
187
|
+
print(f"An error occurred\n\n{req_err}")
|
@@ -23,7 +23,8 @@ from . import (
|
|
23
23
|
get_generation_times,
|
24
24
|
get_full_gridded_forecast,
|
25
25
|
get_gridded_forecast,
|
26
|
-
get_tropical_cyclones
|
26
|
+
get_tropical_cyclones,
|
27
|
+
get_population_weighted_hdd
|
27
28
|
|
28
29
|
)
|
29
30
|
|
@@ -226,6 +227,14 @@ def main():
|
|
226
227
|
tropical_cyclones_parser.add_argument('args', nargs='*',
|
227
228
|
help='[optional: initialization time (YYYYMMDDHH, YYYY-MM-DDTHH, or YYYY-MM-DDTHH:mm:ss)] output_file')
|
228
229
|
|
230
|
+
# Population Weighted HDD Command
|
231
|
+
hdd_parser = subparsers.add_parser('hdd', help='Get population weighted heating degree days (HDD) forecasts')
|
232
|
+
hdd_parser.add_argument('initialization_time', help='Initialization time (YYYYMMDDHH, YYYY-MM-DDTHH, or YYYY-MM-DDTHH:mm:ss)')
|
233
|
+
hdd_parser.add_argument('-i', '--intracycle', action='store_true', help='Use the intracycle forecast')
|
234
|
+
hdd_parser.add_argument('-e', '--ens-member', help='Ensemble member (eg 1 or mean)')
|
235
|
+
hdd_parser.add_argument('-m', '--external-model', help='External model (eg gfs, ifs, hrrr, aifs)')
|
236
|
+
hdd_parser.add_argument('-o', '--output', help='Output file (supports .csv and .json formats)')
|
237
|
+
|
229
238
|
# Initialization Times Command
|
230
239
|
initialization_times_parser = subparsers.add_parser('init_times', help='Get available initialization times for point forecasts')
|
231
240
|
initialization_times_parser.add_argument('-i', '--intracycle', action='store_true', help='Use the intracycle forecast')
|
@@ -527,6 +536,17 @@ def main():
|
|
527
536
|
print("Error: Too many arguments")
|
528
537
|
print("Usage: windborne tropical_cyclones [initialization_time] output_file")
|
529
538
|
|
539
|
+
elif args.command == 'hdd':
|
540
|
+
# Handle population weighted HDD
|
541
|
+
get_population_weighted_hdd(
|
542
|
+
initialization_time=args.initialization_time,
|
543
|
+
intracycle=args.intracycle,
|
544
|
+
ens_member=args.ens_member,
|
545
|
+
external_model=args.external_model,
|
546
|
+
output_file=args.output,
|
547
|
+
print_response=(not args.output)
|
548
|
+
)
|
549
|
+
|
530
550
|
else:
|
531
551
|
parser.print_help()
|
532
552
|
|
@@ -389,3 +389,41 @@ def download_and_save_output(output_file, response, silent=False):
|
|
389
389
|
if not silent:
|
390
390
|
print(f"Error processing the file: {e}")
|
391
391
|
return False
|
392
|
+
|
393
|
+
def get_population_weighted_hdd(initialization_time, intracycle=False, ens_member=None, external_model=None, output_file=None, print_response=False):
|
394
|
+
"""
|
395
|
+
Get population weighted HDD data from the API.
|
396
|
+
"""
|
397
|
+
params = {
|
398
|
+
"initialization_time": initialization_time,
|
399
|
+
"intracycle": intracycle,
|
400
|
+
"ens_member": ens_member,
|
401
|
+
"external_model": external_model
|
402
|
+
}
|
403
|
+
response = make_api_request(f"{FORECASTS_API_BASE_URL}/hdd", params=params, as_json=True)
|
404
|
+
|
405
|
+
if output_file:
|
406
|
+
if output_file.endswith('.csv'):
|
407
|
+
import csv
|
408
|
+
|
409
|
+
# save as csv, with a row for each region, and a column for each date, sorted alphabetically by region
|
410
|
+
regions = sorted(response['hdd'].keys())
|
411
|
+
dates = response['dates']
|
412
|
+
data = [[response['hdd'][region][dates[i]] for region in regions] for i in range(len(dates))]
|
413
|
+
|
414
|
+
with open(output_file, 'w') as f:
|
415
|
+
writer = csv.writer(f)
|
416
|
+
writer.writerow(['Region'] + dates)
|
417
|
+
|
418
|
+
for region in regions:
|
419
|
+
writer.writerow([region] + [response['hdd'][region][date] for date in dates])
|
420
|
+
|
421
|
+
if print_response:
|
422
|
+
dates = response['dates']
|
423
|
+
print(response['hdd']['Alabama'])
|
424
|
+
for region in sorted(response['hdd'].keys()):
|
425
|
+
print(f"{region}:")
|
426
|
+
for i in range(len(dates)):
|
427
|
+
print(f" {dates[i]}: {response['hdd'][region][dates[i]]}")
|
428
|
+
|
429
|
+
return response
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: windborne
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.5
|
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
|
@@ -29,3 +29,5 @@ Run `windborne --help` for more information.
|
|
29
29
|
|
30
30
|
## Further information and help request
|
31
31
|
If you encounter issues or have questions, please ask your WindBorne Systems contact or email data@windbornesystems.com.
|
32
|
+
|
33
|
+
For development of this package, see [README_dev.md](README_dev.md)
|
@@ -1,229 +0,0 @@
|
|
1
|
-
import jwt
|
2
|
-
import time
|
3
|
-
import requests
|
4
|
-
import re
|
5
|
-
import os
|
6
|
-
|
7
|
-
def is_valid_uuid_v4(client_id):
|
8
|
-
return re.fullmatch(r"[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}", client_id) is not None
|
9
|
-
|
10
|
-
|
11
|
-
def is_valid_client_id_format(client_id):
|
12
|
-
return re.fullmatch(r"[a-z0-9_]+", client_id) is not None
|
13
|
-
|
14
|
-
|
15
|
-
def get_api_credentials():
|
16
|
-
client_id = os.getenv('WB_CLIENT_ID')
|
17
|
-
api_key = os.getenv('WB_API_KEY')
|
18
|
-
|
19
|
-
return client_id, api_key
|
20
|
-
|
21
|
-
|
22
|
-
def verify_api_credentials(client_id, api_key):
|
23
|
-
if not client_id and not api_key:
|
24
|
-
print("To access the WindBorne API, set your Client ID and API key by setting the environment variables WB_CLIENT_ID and WB_API_KEY.")
|
25
|
-
print("--------------------------------------")
|
26
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
27
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
28
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
29
|
-
"for instruction on how to set your credentials for code usage.")
|
30
|
-
print("--------------------------------------")
|
31
|
-
print("To get an API key, email data@windbornesystems.com.")
|
32
|
-
exit(80)
|
33
|
-
|
34
|
-
if not client_id:
|
35
|
-
print("To access the WindBorne API, you need to set your Client ID by setting the environment variable WB_CLIENT_ID.")
|
36
|
-
print("--------------------------------------")
|
37
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
38
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
39
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
40
|
-
"for instruction on how to set your credentials for code usage.")
|
41
|
-
print("--------------------------------------")
|
42
|
-
print("To get an API key, email data@windbornesystems.com.")
|
43
|
-
exit(90)
|
44
|
-
|
45
|
-
if not api_key:
|
46
|
-
print("To access the WindBorne API, you need to set your API key by setting the environment variable WB_API_KEY.")
|
47
|
-
print("--------------------------------------")
|
48
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
49
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
50
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
51
|
-
"for instruction on how to set your credentials for code usage.")
|
52
|
-
print("--------------------------------------")
|
53
|
-
print("To get an API key, email data@windbornesystems.com.")
|
54
|
-
exit(91)
|
55
|
-
|
56
|
-
if len(client_id) in [32, 35] and len(api_key) not in [32, 35]:
|
57
|
-
print("Your Client ID and API Key are likely swapped.")
|
58
|
-
print("--------------------------------------")
|
59
|
-
print("Swap them or modify them accordingly to get access to WindBorne API.")
|
60
|
-
print("--------------------------------------")
|
61
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
62
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
63
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
64
|
-
"for instruction on how to set your credentials for code usage.")
|
65
|
-
print("--------------------------------------")
|
66
|
-
print(f"Current Client ID: {client_id}")
|
67
|
-
print(f"Current API Key: {api_key}")
|
68
|
-
exit(95)
|
69
|
-
|
70
|
-
# Validate WB_CLIENT_ID format
|
71
|
-
if is_valid_uuid_v4(client_id):
|
72
|
-
print("Personal API tokens are not yet supported.")
|
73
|
-
print("--------------------------------------")
|
74
|
-
print("You will need to get a globally-authorizing API key.")
|
75
|
-
print("For questions, email data@windbornesystems.com.")
|
76
|
-
exit(1)
|
77
|
-
|
78
|
-
if not (is_valid_uuid_v4(client_id) or is_valid_client_id_format(client_id)):
|
79
|
-
print("Your Client ID is misformatted.")
|
80
|
-
print("--------------------------------------")
|
81
|
-
print("It should either be a valid UUID v4 or consist of only lowercase letters, digits, and underscores ([a-z0-9_]).")
|
82
|
-
print("--------------------------------------")
|
83
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
84
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
85
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
86
|
-
"for instruction on how to set your credentials for code usage.")
|
87
|
-
print("--------------------------------------")
|
88
|
-
print(f"Current Client ID: {client_id}")
|
89
|
-
exit(92)
|
90
|
-
|
91
|
-
# Validate WB_API_KEY for both newer and older formats
|
92
|
-
if api_key.startswith("wb_"):
|
93
|
-
if len(api_key) != 35:
|
94
|
-
print("Your API key is misformatted.")
|
95
|
-
print("--------------------------------------")
|
96
|
-
print("API keys starting with 'wb_' must be 35 characters long (including the 'wb_' prefix).")
|
97
|
-
print("--------------------------------------")
|
98
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
99
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
100
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
101
|
-
"for instruction on how to set your credentials for code usage.")
|
102
|
-
print("--------------------------------------")
|
103
|
-
print(f"Current API key: {api_key}")
|
104
|
-
exit(93)
|
105
|
-
elif len(api_key) != 32: # For early tokens
|
106
|
-
print("Your API key is misformatted.")
|
107
|
-
print("--------------------------------------")
|
108
|
-
print("API keys created in 2023 or earlier must be exactly 32 characters long.")
|
109
|
-
print("--------------------------------------")
|
110
|
-
print("You may refer to https://windbornesystems.com/docs/api/cli#introduction\n"
|
111
|
-
"for instructions on how to set your credentials as environment variables for CLI and Code usage\n\n"
|
112
|
-
"and to https://windbornesystems.com/docs/api/pip_data#introduction\n"
|
113
|
-
"for instruction on how to set your credentials for code usage.")
|
114
|
-
print("--------------------------------------")
|
115
|
-
print(f"Current API key: {api_key}")
|
116
|
-
exit(94)
|
117
|
-
|
118
|
-
|
119
|
-
VERIFIED_WB_CLIENT_ID = None
|
120
|
-
VERIFIED_WB_API_KEY = None
|
121
|
-
|
122
|
-
def get_verified_api_credentials():
|
123
|
-
global VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY
|
124
|
-
|
125
|
-
if VERIFIED_WB_CLIENT_ID is None or VERIFIED_WB_API_KEY is None:
|
126
|
-
VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY = get_api_credentials()
|
127
|
-
verify_api_credentials(VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY)
|
128
|
-
|
129
|
-
return VERIFIED_WB_CLIENT_ID, VERIFIED_WB_API_KEY
|
130
|
-
|
131
|
-
|
132
|
-
def make_api_request(url, params=None, as_json=True, retry_counter=0):
|
133
|
-
"""
|
134
|
-
Make an authenticated request to the WindBorne API.
|
135
|
-
|
136
|
-
This uses a JWT under the hood
|
137
|
-
While basic auth is supported, this method reduces the odds of an improper configuration accidentally leaking the keys
|
138
|
-
|
139
|
-
:param url: The URL to make the request to
|
140
|
-
:param params: The parameters to pass to the request
|
141
|
-
:param as_json: Whether to return the response as JSON or as a requests.Response object
|
142
|
-
:param retry_counter: The number of times the request has been retried
|
143
|
-
:return:
|
144
|
-
"""
|
145
|
-
if retry_counter >= 5:
|
146
|
-
print("Max retries to API reached. Exiting.")
|
147
|
-
exit(1)
|
148
|
-
|
149
|
-
client_id, api_key = get_verified_api_credentials()
|
150
|
-
|
151
|
-
if is_valid_uuid_v4(client_id):
|
152
|
-
token_id = client_id
|
153
|
-
client_id = 'api_token'
|
154
|
-
|
155
|
-
signed_token = jwt.encode({
|
156
|
-
'client_id': client_id,
|
157
|
-
'iat': int(time.time()),
|
158
|
-
'token_id': token_id
|
159
|
-
}, api_key, algorithm='HS256')
|
160
|
-
else:
|
161
|
-
signed_token = jwt.encode({
|
162
|
-
'client_id': client_id,
|
163
|
-
'iat': int(time.time()),
|
164
|
-
}, api_key, algorithm='HS256')
|
165
|
-
|
166
|
-
try:
|
167
|
-
if params:
|
168
|
-
response = requests.get(url, auth=(client_id, signed_token), params=params)
|
169
|
-
else:
|
170
|
-
response = requests.get(url, auth=(client_id, signed_token))
|
171
|
-
|
172
|
-
response.raise_for_status()
|
173
|
-
|
174
|
-
if as_json:
|
175
|
-
return response.json()
|
176
|
-
else:
|
177
|
-
return response
|
178
|
-
|
179
|
-
except requests.exceptions.HTTPError as http_err:
|
180
|
-
if http_err.response.status_code == 403:
|
181
|
-
print("--------------------------------------")
|
182
|
-
print("We couldn't authenticate your request.")
|
183
|
-
print("--------------------------------------")
|
184
|
-
print("You likely don't have permission to access this resource.\n")
|
185
|
-
print("For questions, email data@windbornesystems.com.")
|
186
|
-
elif http_err.response.status_code in [404, 400]:
|
187
|
-
print("-------------------------------------------------------")
|
188
|
-
print("Our server couldn't find the information you requested.")
|
189
|
-
print("-------------------------------------------------------")
|
190
|
-
print(f"URL: {url}")
|
191
|
-
print(f"Error: {http_err.response.status_code}")
|
192
|
-
print("-------------------------------------------------------")
|
193
|
-
if params:
|
194
|
-
print("\nParameters provided:")
|
195
|
-
for key, value in params.items():
|
196
|
-
print(f" {key}: {value}")
|
197
|
-
else:
|
198
|
-
if 'missions/' in url:
|
199
|
-
mission_id = url.split('/missions/')[1].split('/')[0]
|
200
|
-
print(f"Mission ID provided: {mission_id}")
|
201
|
-
print(f"No mission found with id: {mission_id}")
|
202
|
-
print("-------------------------------------------------------")
|
203
|
-
print("Response text:")
|
204
|
-
print(http_err.response.text)
|
205
|
-
return None
|
206
|
-
elif http_err.response.status_code == 502:
|
207
|
-
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
208
|
-
print(f"Underlying error: 502 Bad Gateway")
|
209
|
-
time.sleep(2**retry_counter)
|
210
|
-
return make_api_request(url, params, as_json, retry_counter + 1)
|
211
|
-
else:
|
212
|
-
print(f"Unrecoverable HTTP error occurred \n\n{http_err}")
|
213
|
-
if params:
|
214
|
-
print("\nParameters provided:")
|
215
|
-
for key, value in params.items():
|
216
|
-
print(f" {key}: {value}")
|
217
|
-
exit(1)
|
218
|
-
except requests.exceptions.ConnectionError as conn_err:
|
219
|
-
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
220
|
-
print(f"Underlying error: \n\n{conn_err}")
|
221
|
-
time.sleep(2**retry_counter)
|
222
|
-
return make_api_request(url, params, as_json, retry_counter + 1)
|
223
|
-
except requests.exceptions.Timeout as timeout_err:
|
224
|
-
print(f"Temporary connection failure; sleeping for {2**retry_counter}s before retrying")
|
225
|
-
print(f"Underlying error: \n\n{timeout_err}")
|
226
|
-
time.sleep(2**retry_counter)
|
227
|
-
return make_api_request(url, params, as_json, retry_counter + 1)
|
228
|
-
except requests.exceptions.RequestException as req_err:
|
229
|
-
print(f"An error occurred\n\n{req_err}")
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|