api-24sea 0.1.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.
- api_24sea/__init__.py +17 -0
- api_24sea/datasignals/__init__.py +319 -0
- api_24sea/datasignals/schemas.py +103 -0
- api_24sea/utils.py +63 -0
- api_24sea/version.py +69 -0
- api_24sea-0.1.0.dist-info/LICENSE +674 -0
- api_24sea-0.1.0.dist-info/METADATA +164 -0
- api_24sea-0.1.0.dist-info/RECORD +9 -0
- api_24sea-0.1.0.dist-info/WHEEL +4 -0
api_24sea/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""The `api_24sea` package contains modules that are aimed at helping a user
|
|
3
|
+
interact with the 24SEA API.
|
|
4
|
+
|
|
5
|
+
The modules are:
|
|
6
|
+
|
|
7
|
+
- :mod:`api_24sea.datasignals`: Contains the :class:`DataSignals` class, which
|
|
8
|
+
is an accessor for transforming data signals from the 24SEA API into
|
|
9
|
+
pandas DataFrames.
|
|
10
|
+
|
|
11
|
+
Besides the modules, the package also contains the :mod:`api_24sea.version`
|
|
12
|
+
module, which contains the version number of the package.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from . import datasignals
|
|
16
|
+
|
|
17
|
+
__all__ = ["datasignals"]
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""The module :mod:`py_fatigue.damage.crack_growth` contains all the
|
|
3
|
+
damage models related to the crack growth approach.
|
|
4
|
+
"""
|
|
5
|
+
import datetime
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import requests as req
|
|
10
|
+
from pydantic import validate_call
|
|
11
|
+
|
|
12
|
+
# Local imports
|
|
13
|
+
from .. import utils as U
|
|
14
|
+
from . import schemas as S
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
# delete the accessor to avoid warning
|
|
18
|
+
del pd.DataFrame.datasignals
|
|
19
|
+
except AttributeError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
BASE_URL = "https://api.24sea.eu/routes/v1/"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logging.basicConfig(format="%(message)s", level=logging.INFO)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_authentication(func):
|
|
29
|
+
"""Check authentication before making the request to the 24SEA API."""
|
|
30
|
+
|
|
31
|
+
def wrapper(self, *args, **kwargs):
|
|
32
|
+
"""Wrapper function to check authentication."""
|
|
33
|
+
# fmt: off
|
|
34
|
+
if not isinstance(self.auth, req.auth.HTTPBasicAuth) or not self.authenticated:
|
|
35
|
+
raise U.AuthenticationError(
|
|
36
|
+
"\033[31;1mAuthentication needed before querying the metrics.\n"
|
|
37
|
+
"\033[0mUse the \033[34;1mdatasignals.\033[0mauthenticate("
|
|
38
|
+
"\033[32;1m<username>\033[0m, \033[32;1m<password>\033[0m) "
|
|
39
|
+
"method."
|
|
40
|
+
)
|
|
41
|
+
return func(self, *args, **kwargs)
|
|
42
|
+
# fmt: on
|
|
43
|
+
|
|
44
|
+
return wrapper
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pd.api.extensions.register_dataframe_accessor("datasignals")
|
|
48
|
+
class DataSignals:
|
|
49
|
+
"""Accessor for working with data signals coming from the 24SEA API."""
|
|
50
|
+
|
|
51
|
+
def __init__(self, pandasdata: pd.DataFrame):
|
|
52
|
+
self.data = pandasdata
|
|
53
|
+
self.base_url: str = f"{BASE_URL}datasignals/"
|
|
54
|
+
self.username: str | None = None
|
|
55
|
+
self.password: str | None = None
|
|
56
|
+
self.auth: req.auth.HTTPBasicAuth | None = None
|
|
57
|
+
self.authenticated: bool = False
|
|
58
|
+
self.metrics_overview: pd.DataFrame | None = None
|
|
59
|
+
self.selected_metrics: pd.DataFrame | None = None
|
|
60
|
+
|
|
61
|
+
@validate_call
|
|
62
|
+
def authenticate(self, username: str, password: str) -> None:
|
|
63
|
+
"""Authenticate the user with the 24SEA API. Additionally, define
|
|
64
|
+
the ``metrics_overview`` dataframe.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
username : str
|
|
69
|
+
The username to authenticate.
|
|
70
|
+
password : str
|
|
71
|
+
The password to authenticate.
|
|
72
|
+
"""
|
|
73
|
+
# -- Step 1: Authenticate and check the credentials
|
|
74
|
+
self.username = username
|
|
75
|
+
self.password = password
|
|
76
|
+
self.auth = req.auth.HTTPBasicAuth(self.username, self.password)
|
|
77
|
+
# fmt: off
|
|
78
|
+
try:
|
|
79
|
+
r_profile = U.handle_request(
|
|
80
|
+
f"{self.base_url}profile",
|
|
81
|
+
{"username": self.username},
|
|
82
|
+
self.auth,
|
|
83
|
+
{"accept": "application/json"},
|
|
84
|
+
)
|
|
85
|
+
self.authenticated = True
|
|
86
|
+
logging.info("\033[32;1mThis dataframe has now access to https://api.24sea.eu/.\033[0m") # noqa: E501 # pylint: disable=C0301
|
|
87
|
+
except req.exceptions.HTTPError:
|
|
88
|
+
raise U.AuthenticationError("\033[31;1mThe username and/or password are incorrect.\033[0m") # noqa: E501 # pylint: disable=C0301
|
|
89
|
+
# fmt: on
|
|
90
|
+
# -- Step 2: Define the metrics_overview dataframe
|
|
91
|
+
if self.metrics_overview is not None:
|
|
92
|
+
return None
|
|
93
|
+
logging.info("Now getting your metrics_overview table...")
|
|
94
|
+
r_metrics = U.handle_request(
|
|
95
|
+
f"{self.base_url}metrics",
|
|
96
|
+
{"project": None, "locations": None, "metrics": None},
|
|
97
|
+
self.auth,
|
|
98
|
+
{"accept": "application/json"},
|
|
99
|
+
)
|
|
100
|
+
# fmt: off
|
|
101
|
+
if not isinstance(r_metrics, type(None)):
|
|
102
|
+
try:
|
|
103
|
+
m_ = pd.DataFrame(r_metrics.json())
|
|
104
|
+
except Exception:
|
|
105
|
+
raise U.ProfileError(f"\033[31;1mThe metrics overview is empty. This is your profile information:" # noqa: E501 # pylint: disable=C0301
|
|
106
|
+
f"\n {r_profile.json()}")
|
|
107
|
+
if m_.empty:
|
|
108
|
+
raise U.ProfileError(f"\033[31;1mThe metrics overview is empty. This is your profile information:" # noqa: E501 # pylint: disable=C0301
|
|
109
|
+
f"\n {r_profile.json()}")
|
|
110
|
+
try:
|
|
111
|
+
s_ = m_.apply(lambda x: x["metric"]
|
|
112
|
+
.replace(x["statistic"], "")
|
|
113
|
+
.replace(x["short_hand"], "")
|
|
114
|
+
.strip(), axis=1).str.strip("_").str.split("_", expand=True) # noqa: E501 # pylint: disable=C0301
|
|
115
|
+
s_.columns = ["site_id", "location_id"]
|
|
116
|
+
# fmt: on
|
|
117
|
+
except Exception:
|
|
118
|
+
self.metrics_overview = m_
|
|
119
|
+
self.metrics_overview = pd.concat([m_, s_], axis=1)
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
@check_authentication
|
|
123
|
+
@validate_call
|
|
124
|
+
def get_metrics(
|
|
125
|
+
self,
|
|
126
|
+
site: str | None = None,
|
|
127
|
+
locations: str | list[str] | None = None,
|
|
128
|
+
metrics: str | list[str] | None = None,
|
|
129
|
+
headers: dict[str, str] | None = None,
|
|
130
|
+
) -> list[dict[str, str | None]] | None:
|
|
131
|
+
"""Get the metrics names for a site, provided the following parameters.
|
|
132
|
+
|
|
133
|
+
Parameters
|
|
134
|
+
----------
|
|
135
|
+
site : str | None
|
|
136
|
+
The site name. If None, the queryable metrics for all sites
|
|
137
|
+
will be returned, and the locations and metrics parameters will be
|
|
138
|
+
ignored.
|
|
139
|
+
locations : list[str] | None
|
|
140
|
+
The locations for which to get the metrics. If None, all locations
|
|
141
|
+
will be considered.
|
|
142
|
+
metrics : list[str] | None
|
|
143
|
+
The metrics to get. They can be specified as regular expressions.
|
|
144
|
+
If None, all metrics will be considered.
|
|
145
|
+
For example:
|
|
146
|
+
* metrics=["^ACC", "^DEM"] will return all the metrics that start
|
|
147
|
+
with ACC or DEM,
|
|
148
|
+
* Similarly, metrics=["windspeed$", "winddirection$"] will return
|
|
149
|
+
all the metrics that end with windspeed and winddirection,
|
|
150
|
+
* and metrics=[".*WF_A01.*",".*WF_A02.*"] will return all metrics
|
|
151
|
+
that contain WF_A01 or WF_A02.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
list[dict[str, str | None]] | None
|
|
156
|
+
The metrics names for the given site, locations and metrics.
|
|
157
|
+
|
|
158
|
+
.. note::
|
|
159
|
+
This class method is legacy because it does not add functionality to
|
|
160
|
+
the DataSignals pandas accessor.
|
|
161
|
+
|
|
162
|
+
"""
|
|
163
|
+
url = f"{self.base_url}metrics"
|
|
164
|
+
# fmt: on
|
|
165
|
+
if headers is None:
|
|
166
|
+
headers = {"accept": "application/json"}
|
|
167
|
+
if site is None:
|
|
168
|
+
params = {}
|
|
169
|
+
if isinstance(locations, list):
|
|
170
|
+
locations = ",".join(locations)
|
|
171
|
+
if isinstance(metrics, list):
|
|
172
|
+
metrics = ",".join(metrics)
|
|
173
|
+
params = {
|
|
174
|
+
"project": site,
|
|
175
|
+
"locations": locations,
|
|
176
|
+
"metrics": metrics,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
r_ = U.handle_request(url, params, self.auth, headers)
|
|
180
|
+
|
|
181
|
+
# Set the return type of the get_metrics method to the Metrics schema
|
|
182
|
+
return r_.json() # type: ignore
|
|
183
|
+
|
|
184
|
+
@check_authentication
|
|
185
|
+
@validate_call
|
|
186
|
+
def get_data(
|
|
187
|
+
self,
|
|
188
|
+
sites: list | str | None,
|
|
189
|
+
locations: list | str | None,
|
|
190
|
+
metrics: list | str | None,
|
|
191
|
+
start_timestamp: str | datetime.datetime,
|
|
192
|
+
end_timestamp: str | datetime.datetime,
|
|
193
|
+
outer_join_on_timestamp: bool = False,
|
|
194
|
+
headers: dict[str, str] | None = None,
|
|
195
|
+
):
|
|
196
|
+
"""Get the data signals from the 24SEA API.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
filters : dict
|
|
201
|
+
The filters to apply when querying the data signals.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
pd.DataFrame
|
|
206
|
+
The DataFrame containing the data signals.
|
|
207
|
+
"""
|
|
208
|
+
# Clean the DataFrame
|
|
209
|
+
self.data = pd.DataFrame()
|
|
210
|
+
# -- Step 1: Build the query object from GetData
|
|
211
|
+
query = S.GetData(
|
|
212
|
+
start_timestamp=start_timestamp,
|
|
213
|
+
end_timestamp=end_timestamp,
|
|
214
|
+
sites=sites,
|
|
215
|
+
locations=locations,
|
|
216
|
+
metrics=metrics,
|
|
217
|
+
headers=headers,
|
|
218
|
+
outer_join_on_timestamp=outer_join_on_timestamp,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
match query.sites, query.locations:
|
|
222
|
+
case None, None:
|
|
223
|
+
query_str = "metric.str.contains(@query.metrics, case=False, regex=True)"
|
|
224
|
+
case None, _:
|
|
225
|
+
query_str = (
|
|
226
|
+
"(location.str.lower() == @query.locations or location_id.str.lower() == @query.locations) "
|
|
227
|
+
"and metric.str.contains(@query.metrics, case=False, regex=True)"
|
|
228
|
+
)
|
|
229
|
+
case _, None:
|
|
230
|
+
query_str = (
|
|
231
|
+
"(site.str.lower() == @query.sites or site_id.str.lower() == @query.sites) "
|
|
232
|
+
"and metric.str.contains(@query.metrics, case=False, regex=True)"
|
|
233
|
+
)
|
|
234
|
+
case _, _:
|
|
235
|
+
query_str = (
|
|
236
|
+
"(site_id.str.lower() == @query.sites or site.str.lower() == @query.sites) "
|
|
237
|
+
"and (location.str.lower() == @query.locations or location_id.str.lower() == @query.locations) "
|
|
238
|
+
"and metric.str.contains(@query.metrics, case=False, regex=True)"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
logging.info(
|
|
242
|
+
f"\033[30;1mQuery:\033[0;34m {query_str.replace(" and ", "\n and ")}\n"
|
|
243
|
+
)
|
|
244
|
+
self.selected_metrics = self.metrics_overview.query(query_str).pipe( # type: ignore # noqa: E501 # pylint: disable=E501
|
|
245
|
+
lambda df: df.sort_values(
|
|
246
|
+
["site", "location", "data_group", "short_hand", "statistic"],
|
|
247
|
+
ascending=[True, True, False, True, True],
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
logging.info("\033[32;1mMetrics selected for the query:\033[0m\n")
|
|
251
|
+
# fmt: off
|
|
252
|
+
logging.info(self.selected_metrics[["metric", "unit_str", "data_group",
|
|
253
|
+
"location", "site"]]
|
|
254
|
+
.reset_index(drop=True))
|
|
255
|
+
# fmt: on
|
|
256
|
+
data_frames = []
|
|
257
|
+
grouped_metrics = self.selected_metrics.groupby(["site", "location"])
|
|
258
|
+
|
|
259
|
+
# return grouped_metrics
|
|
260
|
+
logging.info(
|
|
261
|
+
f"\n\033[32;1mRequested time range:\033[0;33m "
|
|
262
|
+
f"From {str(query.start_timestamp)[:-4].replace("T", " ")} "
|
|
263
|
+
f"To {str(query.end_timestamp)[:-4].replace("T", " ")}\n\033[0m"
|
|
264
|
+
)
|
|
265
|
+
import concurrent.futures
|
|
266
|
+
|
|
267
|
+
def fetch_data(site, location, group):
|
|
268
|
+
# fmt: off
|
|
269
|
+
s_ = "• " + ",".join(group["metric"].tolist()).replace(",", "\n • ") # noqa: E501 # pylint: disable=C0301
|
|
270
|
+
logging.info(f"\033[32;1m\033[32;1m⏳ Getting data for {site} - "
|
|
271
|
+
f"{location}...\n Metrics: \033[0;33m{s_}\n\033[0m")
|
|
272
|
+
# fmt: on
|
|
273
|
+
r_ = U.handle_request(
|
|
274
|
+
f"{self.base_url}data",
|
|
275
|
+
{
|
|
276
|
+
"start_timestamp": query.start_timestamp,
|
|
277
|
+
"end_timestamp": query.end_timestamp,
|
|
278
|
+
"project": [site],
|
|
279
|
+
"location": [location],
|
|
280
|
+
"metrics": ",".join(group["metric"].tolist()),
|
|
281
|
+
},
|
|
282
|
+
self.auth,
|
|
283
|
+
query.headers,
|
|
284
|
+
)
|
|
285
|
+
return pd.DataFrame(r_.json())
|
|
286
|
+
|
|
287
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
288
|
+
# fmt: off
|
|
289
|
+
future_to_data = {
|
|
290
|
+
executor.submit(fetch_data,
|
|
291
|
+
site,
|
|
292
|
+
location,
|
|
293
|
+
group): (site, location)
|
|
294
|
+
for (site, location), group in grouped_metrics
|
|
295
|
+
}
|
|
296
|
+
# fmt: on
|
|
297
|
+
for future in concurrent.futures.as_completed(future_to_data):
|
|
298
|
+
data_frames.append(future.result())
|
|
299
|
+
# data_frames.append(pd.DataFrame(r_.json()))
|
|
300
|
+
|
|
301
|
+
# if left_join_on_timestamp is True, lose the location and site columns
|
|
302
|
+
# and join on timestamp
|
|
303
|
+
if outer_join_on_timestamp:
|
|
304
|
+
for i, df in enumerate(data_frames):
|
|
305
|
+
data_frames[i] = df.set_index("timestamp")
|
|
306
|
+
# drop site and location
|
|
307
|
+
data_frames[i].drop(["site", "location"], axis=1, inplace=True)
|
|
308
|
+
self.data = pd.concat(
|
|
309
|
+
[self.data] + data_frames, axis=1, join="outer"
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
self.data = pd.concat([self.data] + data_frames, ignore_index=True)
|
|
313
|
+
# To access the data, use the .data attribute of the datasignals accessor
|
|
314
|
+
logging.info("\033[32;1m✔️ Data successfully retrieved.\033[0m")
|
|
315
|
+
print(
|
|
316
|
+
"To access the data, use the \033[30;1m.data\033[0m attribute "
|
|
317
|
+
"of the \033[30;1m.datasignals\033[0m accessor."
|
|
318
|
+
)
|
|
319
|
+
return self.data
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Data signals types."""
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ValidationInfo, field_validator
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Metric(BaseModel):
|
|
10
|
+
"""A pydantic schema for the metrics names."""
|
|
11
|
+
|
|
12
|
+
start_timestamp: str
|
|
13
|
+
end_timestamp: str
|
|
14
|
+
site: str
|
|
15
|
+
location: str
|
|
16
|
+
data_group: str | None
|
|
17
|
+
metric: str
|
|
18
|
+
statistic: str | None
|
|
19
|
+
short_hand: str | None
|
|
20
|
+
unit_str: str | None
|
|
21
|
+
print_str: str | None
|
|
22
|
+
description: str | None
|
|
23
|
+
crud_privileges: str | None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Metrics(BaseModel):
|
|
27
|
+
"""A pydantic schema for the metrics names."""
|
|
28
|
+
|
|
29
|
+
metrics: list[Metric]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GetData(BaseModel):
|
|
33
|
+
"""A pydantic schema for the data signals."""
|
|
34
|
+
|
|
35
|
+
start_timestamp: str | datetime.datetime
|
|
36
|
+
end_timestamp: str | datetime.datetime
|
|
37
|
+
sites: str | list[str] | None
|
|
38
|
+
locations: str | list[str] | None
|
|
39
|
+
metrics: str | list[str] | None
|
|
40
|
+
outer_join_on_timestamp: bool | None
|
|
41
|
+
headers: dict[str, str] | None
|
|
42
|
+
|
|
43
|
+
@field_validator("start_timestamp", "end_timestamp", mode="before")
|
|
44
|
+
def validate_timestamp(cls, v: str | datetime.datetime) -> str:
|
|
45
|
+
"""Validate the timestamps."""
|
|
46
|
+
if isinstance(v, str):
|
|
47
|
+
try:
|
|
48
|
+
datetime.datetime.strptime(v, "%Y-%m-%dT%H:%M:%SZ")
|
|
49
|
+
except ValueError:
|
|
50
|
+
raise ValueError(
|
|
51
|
+
"Incorrect start timestamp format, expected ISO 8601."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if isinstance(v, datetime.datetime):
|
|
55
|
+
# Enforce timezone UTC as well
|
|
56
|
+
return v.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
57
|
+
|
|
58
|
+
return v
|
|
59
|
+
|
|
60
|
+
@field_validator("end_timestamp")
|
|
61
|
+
def validate_end_timestamp(cls, v, info: ValidationInfo):
|
|
62
|
+
"""Validate the end timestamp."""
|
|
63
|
+
if "start_timestamp" in info.data and v < info.data["start_timestamp"]:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
"End timestamp must be greater than start timestamp."
|
|
66
|
+
)
|
|
67
|
+
return v
|
|
68
|
+
|
|
69
|
+
@field_validator("sites", "locations", mode="before")
|
|
70
|
+
def validate_sites_locations(cls, v):
|
|
71
|
+
"""Validate and normalize sites and locations."""
|
|
72
|
+
if isinstance(v, str):
|
|
73
|
+
v = [v]
|
|
74
|
+
if isinstance(v, list):
|
|
75
|
+
v = [item.lower() for item in v]
|
|
76
|
+
return v
|
|
77
|
+
|
|
78
|
+
@field_validator("metrics", mode="before")
|
|
79
|
+
def validate_metrics(cls, v):
|
|
80
|
+
"""Validate and normalize metrics."""
|
|
81
|
+
if isinstance(v, str):
|
|
82
|
+
v = [v]
|
|
83
|
+
if isinstance(v, list):
|
|
84
|
+
# fmt: off
|
|
85
|
+
v = [item.replace(" ", ".*")
|
|
86
|
+
.replace("_", ".*")
|
|
87
|
+
.replace("-", ".*") for item in v]
|
|
88
|
+
# fmt: on
|
|
89
|
+
return "|".join(v)
|
|
90
|
+
|
|
91
|
+
@field_validator("outer_join_on_timestamp", mode="before")
|
|
92
|
+
def validate_outer_join_on_timestamp(cls, v):
|
|
93
|
+
"""Validate the outer join on timestamp."""
|
|
94
|
+
if v is None:
|
|
95
|
+
return False
|
|
96
|
+
return v
|
|
97
|
+
|
|
98
|
+
@field_validator("headers", mode="before")
|
|
99
|
+
def validate_headers(cls, v):
|
|
100
|
+
"""Validate the headers."""
|
|
101
|
+
if v is None:
|
|
102
|
+
return {"accept": "application/json"}
|
|
103
|
+
return v
|
api_24sea/utils.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Utility functions and classes."""
|
|
3
|
+
|
|
4
|
+
import requests as req
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AuthenticationError(Exception):
|
|
8
|
+
"""An exception to raise when the user is not authenticated."""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ProfileError(Exception):
|
|
14
|
+
"""An exception to raise when the user is authenticated, but its profile
|
|
15
|
+
is not properly configured."""
|
|
16
|
+
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_request(
|
|
21
|
+
url: str, params: dict, auth: req.auth.HTTPBasicAuth, headers: dict
|
|
22
|
+
) -> req.models.Response:
|
|
23
|
+
"""Handle the request to the 24SEA API and manage errors.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
url : str
|
|
28
|
+
The URL to which to send the request.
|
|
29
|
+
params : dict
|
|
30
|
+
The parameters to send with the request.
|
|
31
|
+
auth : requests.auth.HTTPBasicAuth
|
|
32
|
+
The authentication object.
|
|
33
|
+
headers : dict
|
|
34
|
+
The headers to send with the request.
|
|
35
|
+
|
|
36
|
+
Returns
|
|
37
|
+
-------
|
|
38
|
+
requests.models.Response
|
|
39
|
+
The response object if the request was successful, otherwise error.
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
r_ = req.get(url, params=params, auth=auth, headers=headers)
|
|
43
|
+
|
|
44
|
+
if r_.status_code in [400, 401, 403, 404, 502, 503, 504]:
|
|
45
|
+
print(f"Request failed because: \033[31;1m{r_.text}\033[0m")
|
|
46
|
+
r_.raise_for_status()
|
|
47
|
+
# this will handle all other errors
|
|
48
|
+
elif r_.status_code == 500:
|
|
49
|
+
# fmt: off
|
|
50
|
+
print("\033[31;1mInternal server error. You will need to contact "
|
|
51
|
+
"support at \033[32;1;4msupport.api@24sea.eu\033[0m")
|
|
52
|
+
# fmt: on
|
|
53
|
+
r_.raise_for_status()
|
|
54
|
+
elif r_.status_code > 400:
|
|
55
|
+
# fmt: off
|
|
56
|
+
print("Request failed with status code: "
|
|
57
|
+
f"\033[31;1m{r_.status_code}\033[0m")
|
|
58
|
+
# fmt: on
|
|
59
|
+
r_.raise_for_status()
|
|
60
|
+
except (req.exceptions.ConnectionError, req.exceptions.Timeout) as exc:
|
|
61
|
+
print(f" Request failed because: \033[31;1m{exc}\033[0m")
|
|
62
|
+
raise exc
|
|
63
|
+
return r_
|
api_24sea/version.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
This module exposes the current version of the package
|
|
4
|
+
so the version can be retreived directly within other code
|
|
5
|
+
files. The version assumes the semver notation (major.minor.patch-release.num).
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
Import the version as constant, dict or tuple:
|
|
9
|
+
|
|
10
|
+
>>> from api_24sea.version import __version__
|
|
11
|
+
|
|
12
|
+
>>> from api_24sea.version import parse_version
|
|
13
|
+
>>> parse_version(__version__)
|
|
14
|
+
|
|
15
|
+
Attributes:
|
|
16
|
+
__version__ (str): Current api_24sea version.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
import re
|
|
20
|
+
from typing import NamedTuple, Optional
|
|
21
|
+
|
|
22
|
+
__version__: str = "0.1.0"
|
|
23
|
+
|
|
24
|
+
_REGEX = "".join(
|
|
25
|
+
[
|
|
26
|
+
r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)",
|
|
27
|
+
r"(?:\-(?P<release>.*)\.(?P<num>\d+))?",
|
|
28
|
+
]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Version(NamedTuple):
|
|
33
|
+
"""Convenience structure for interpreting the version information"""
|
|
34
|
+
|
|
35
|
+
major: int
|
|
36
|
+
minor: int
|
|
37
|
+
patch: int
|
|
38
|
+
release: Optional[str] = None
|
|
39
|
+
num: Optional[int] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def parse_version(version: str) -> Version:
|
|
43
|
+
"""Converts the given version string to a named tuple per the semantic
|
|
44
|
+
version guidelines"""
|
|
45
|
+
match = re.search(_REGEX, version)
|
|
46
|
+
if not match:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Version '{version}' does not comply with the semantic"
|
|
49
|
+
"versioning naming scheme"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
major = int(match.group("major"))
|
|
53
|
+
minor = int(match.group("minor"))
|
|
54
|
+
patch = int(match.group("patch"))
|
|
55
|
+
release = match.group("release")
|
|
56
|
+
|
|
57
|
+
num = None # type: Optional[int]
|
|
58
|
+
try:
|
|
59
|
+
num = int(match.group("num"))
|
|
60
|
+
except TypeError:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
return Version(
|
|
64
|
+
major=major,
|
|
65
|
+
minor=minor,
|
|
66
|
+
patch=patch,
|
|
67
|
+
release=release,
|
|
68
|
+
num=num,
|
|
69
|
+
)
|