sws-api-client 3.2.0__tar.gz → 3.3.0b1__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.
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/PKG-INFO +1 -1
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/pyproject.toml +1 -1
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/__init__.py +1 -1
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/sws_api_client.py +22 -10
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/tasks.py +6 -0
- sws_api_client-3.3.0b1/sws_api_client/token.py +93 -0
- sws_api_client-3.2.0/sws_api_client/token.py +0 -47
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/.gitignore +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/LICENSE +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/README.md +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/auth.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/campaign_manager.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/case_agnostic_model.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/codelist.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/data_retrieval.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/datasets.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/datatable.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/db.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/discover.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/files.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/flaglist.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/generic_models.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/harvester.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/mail.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/metadata_instances.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/plugins.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/s3.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/schemas/validation-report.schema.json +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/sessions.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/tags.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/users.py +0 -0
- {sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/validation_report.py +0 -0
|
@@ -8,6 +8,7 @@ import argparse
|
|
|
8
8
|
import json
|
|
9
9
|
import logging
|
|
10
10
|
import os
|
|
11
|
+
from collections.abc import Callable
|
|
11
12
|
|
|
12
13
|
import boto3
|
|
13
14
|
|
|
@@ -45,6 +46,7 @@ class SwsApiClient:
|
|
|
45
46
|
current_task_id: str | None,
|
|
46
47
|
current_execution_id: str | None,
|
|
47
48
|
object_cache: bool = False,
|
|
49
|
+
credentials_provider: Callable[[], AuthClientCredentials] | None = None,
|
|
48
50
|
) -> None:
|
|
49
51
|
"""Initialize the SWS API client.
|
|
50
52
|
|
|
@@ -55,17 +57,18 @@ class SwsApiClient:
|
|
|
55
57
|
current_task_id: Optional current task identifier
|
|
56
58
|
current_execution_id: Optional current execution identifier
|
|
57
59
|
object_cache: Whether to enable client-side object caching (e.g. for codelists, dataset info)
|
|
60
|
+
credentials_provider: Optional callable returning fresh credentials, used to
|
|
61
|
+
retry a failed token request (e.g. after AWS rotates the secret)
|
|
58
62
|
"""
|
|
59
63
|
|
|
60
64
|
logger.info("Initializing SwsApiClient")
|
|
61
65
|
|
|
62
66
|
self.sws_endpoint = sws_endpoint
|
|
63
67
|
self.sws_token = sws_token
|
|
64
|
-
self.authclient = authclient
|
|
65
68
|
self.current_task_id = current_task_id
|
|
66
69
|
self.current_execution_id = current_execution_id
|
|
67
70
|
self.object_cache = object_cache
|
|
68
|
-
self.token = Token(authclient)
|
|
71
|
+
self.token = Token(authclient, credentials_provider)
|
|
69
72
|
self.is_debug = self.check_debug()
|
|
70
73
|
self.discoverable = Discover(
|
|
71
74
|
sws_endpoint=sws_endpoint, sws_token=sws_token, token=self.token
|
|
@@ -74,6 +77,11 @@ class SwsApiClient:
|
|
|
74
77
|
f"SwsApiClient initialized with endpoint {sws_endpoint} and token {sws_token}"
|
|
75
78
|
)
|
|
76
79
|
|
|
80
|
+
@property
|
|
81
|
+
def authclient(self) -> AuthClientCredentials:
|
|
82
|
+
"""Current auth credentials, updated when the secret is refreshed."""
|
|
83
|
+
return self.token.authclient
|
|
84
|
+
|
|
77
85
|
@classmethod
|
|
78
86
|
def __get_authclient_credentials_from_secret(
|
|
79
87
|
cls, env_name="SWS_USER_CREDENTIALS_SECRET_NAME"
|
|
@@ -95,13 +103,12 @@ class SwsApiClient:
|
|
|
95
103
|
raise ValueError(
|
|
96
104
|
f"Secret name not found in environment variable: {env_name}"
|
|
97
105
|
)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
cls
|
|
101
|
-
)
|
|
106
|
+
if not hasattr(cls, "secret_manager"):
|
|
107
|
+
cls.session = getattr(cls, "session", None) or boto3.Session()
|
|
108
|
+
cls.secret_manager = cls.session.client("secretsmanager")
|
|
102
109
|
response = cls.secret_manager.get_secret_value(SecretId=secret_name)
|
|
103
110
|
secretContent = json.loads(response["SecretString"])
|
|
104
|
-
logger.debug(f"Secret fetched successfully
|
|
111
|
+
logger.debug(f"Secret {secret_name} fetched successfully")
|
|
105
112
|
return AuthClientCredentials(
|
|
106
113
|
clientId=secretContent["ID"],
|
|
107
114
|
clientSecret=secretContent["SECRET"],
|
|
@@ -131,6 +138,7 @@ class SwsApiClient:
|
|
|
131
138
|
logger.debug(
|
|
132
139
|
f"Creating SwsApiClient from environment variables: {sws_endpoint_env}, {authclient_secret_name}"
|
|
133
140
|
)
|
|
141
|
+
credentials_provider = None
|
|
134
142
|
if (
|
|
135
143
|
os.getenv("SWS_AUTH_CLIENTID")
|
|
136
144
|
and os.getenv("SWS_AUTH_CLIENTSECRET")
|
|
@@ -148,11 +156,14 @@ class SwsApiClient:
|
|
|
148
156
|
raise ValueError(
|
|
149
157
|
f"You need ({authclient_secret_name}) or (SWS_AUTH_CLIENTID, SWS_AUTH_CLIENTSECRET, SWS_AUTH_SCOPE, SWS_AUTH_TOKENENDPOINT) environment variables to be set"
|
|
150
158
|
)
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
|
|
160
|
+
# Re-read on token failures, so a secret rotated by AWS is picked up.
|
|
161
|
+
def credentials_provider() -> AuthClientCredentials:
|
|
162
|
+
return cls.__get_authclient_credentials_from_secret(
|
|
153
163
|
env_name=authclient_secret_name
|
|
154
164
|
)
|
|
155
|
-
|
|
165
|
+
|
|
166
|
+
authclient = credentials_provider()
|
|
156
167
|
|
|
157
168
|
sws_token = os.getenv("SWS_TOKEN")
|
|
158
169
|
if not sws_token:
|
|
@@ -168,6 +179,7 @@ class SwsApiClient:
|
|
|
168
179
|
current_task_id=os.getenv("TASK_ID"),
|
|
169
180
|
current_execution_id=os.getenv("EXECUTION_ID"),
|
|
170
181
|
authclient=authclient,
|
|
182
|
+
credentials_provider=credentials_provider,
|
|
171
183
|
)
|
|
172
184
|
|
|
173
185
|
@classmethod
|
|
@@ -335,6 +335,7 @@ class TaskManager:
|
|
|
335
335
|
public: bool = False,
|
|
336
336
|
retry: bool = False,
|
|
337
337
|
emailNotification: bool = False,
|
|
338
|
+
max_duration: int | None = None,
|
|
338
339
|
) -> TaskCreateResponse:
|
|
339
340
|
"""Create a new plugin execution task.
|
|
340
341
|
|
|
@@ -351,6 +352,8 @@ class TaskManager:
|
|
|
351
352
|
public: Whether task is public
|
|
352
353
|
retry: Whether task can be retried
|
|
353
354
|
emailNotification: Whether to send email notifications
|
|
355
|
+
max_duration: Max duration of the task in seconds; when None it is not sent
|
|
356
|
+
and the task manager's default applies
|
|
354
357
|
|
|
355
358
|
Returns:
|
|
356
359
|
TaskCreateResponse: Response containing the created task ID
|
|
@@ -398,6 +401,9 @@ class TaskManager:
|
|
|
398
401
|
"parentTaskId": parentTaskId,
|
|
399
402
|
}
|
|
400
403
|
|
|
404
|
+
if max_duration is not None:
|
|
405
|
+
data["config"]["maxDuration"] = max_duration
|
|
406
|
+
|
|
401
407
|
if group:
|
|
402
408
|
data["group"] = group
|
|
403
409
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from sws_api_client.auth import AuthClientCredentials
|
|
9
|
+
from sws_api_client.case_agnostic_model import CaseAgnosticModel
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
# Wait before each retry. The credentials are refreshed before every retry, so a
|
|
14
|
+
# secret rotated by AWS while a long task is running gets picked up.
|
|
15
|
+
RETRY_DELAYS_SECONDS = (30, 60)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TokenModel(CaseAgnosticModel):
|
|
19
|
+
access_token: str
|
|
20
|
+
expires_on: datetime.datetime
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Token:
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
authclient: AuthClientCredentials,
|
|
27
|
+
credentials_provider: Callable[[], AuthClientCredentials] | None = None,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Args:
|
|
31
|
+
authclient: Credentials used to request access tokens.
|
|
32
|
+
credentials_provider: Optional callable returning fresh credentials. When set,
|
|
33
|
+
a failed token request is retried with credentials it returns.
|
|
34
|
+
"""
|
|
35
|
+
self.authclient = authclient
|
|
36
|
+
self.credentials_provider = credentials_provider
|
|
37
|
+
self.token = self.get_token()
|
|
38
|
+
|
|
39
|
+
def get_token(self) -> TokenModel:
|
|
40
|
+
if (
|
|
41
|
+
hasattr(self, "token")
|
|
42
|
+
and self.token.expires_on is not None
|
|
43
|
+
and self.token.expires_on >= datetime.datetime.now()
|
|
44
|
+
):
|
|
45
|
+
return self.token
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
self.token = self._request_token()
|
|
49
|
+
except requests.RequestException:
|
|
50
|
+
if self.credentials_provider is None:
|
|
51
|
+
raise
|
|
52
|
+
self.token = self._retry_with_fresh_credentials()
|
|
53
|
+
logger.debug(f"New token, expires on {self.token.expires_on}")
|
|
54
|
+
return self.token
|
|
55
|
+
|
|
56
|
+
def _retry_with_fresh_credentials(self) -> TokenModel:
|
|
57
|
+
retries = len(RETRY_DELAYS_SECONDS)
|
|
58
|
+
for attempt, delay in enumerate(RETRY_DELAYS_SECONDS, start=1):
|
|
59
|
+
logger.warning(
|
|
60
|
+
"Token request failed, refreshing credentials and retrying in %ss (%s/%s)",
|
|
61
|
+
delay,
|
|
62
|
+
attempt,
|
|
63
|
+
retries,
|
|
64
|
+
)
|
|
65
|
+
time.sleep(delay)
|
|
66
|
+
self.authclient = self.credentials_provider()
|
|
67
|
+
try:
|
|
68
|
+
return self._request_token()
|
|
69
|
+
except requests.RequestException:
|
|
70
|
+
if attempt == retries:
|
|
71
|
+
logger.error(
|
|
72
|
+
"Token request still failing after %s retries, giving up",
|
|
73
|
+
retries,
|
|
74
|
+
)
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
def _request_token(self) -> TokenModel:
|
|
78
|
+
response = requests.post(
|
|
79
|
+
self.authclient.token_endpoint,
|
|
80
|
+
data={
|
|
81
|
+
"grant_type": "client_credentials",
|
|
82
|
+
"client_id": self.authclient.client_id,
|
|
83
|
+
"client_secret": self.authclient.client_secret,
|
|
84
|
+
"scope": self.authclient.scope,
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
response.raise_for_status()
|
|
88
|
+
result = response.json()
|
|
89
|
+
return TokenModel(
|
|
90
|
+
access_token=result["access_token"],
|
|
91
|
+
expires_on=datetime.datetime.now()
|
|
92
|
+
+ datetime.timedelta(seconds=result["expires_in"]),
|
|
93
|
+
)
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import datetime
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
|
|
7
|
-
from sws_api_client.case_agnostic_model import CaseAgnosticModel
|
|
8
|
-
|
|
9
|
-
logger = logging.getLogger(__name__)
|
|
10
|
-
from sws_api_client.auth import AuthClientCredentials
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class TokenModel(CaseAgnosticModel):
|
|
14
|
-
access_token: str
|
|
15
|
-
expires_on: datetime.datetime
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Token:
|
|
19
|
-
def __init__(self, authclient: AuthClientCredentials) -> None:
|
|
20
|
-
self.authclient = authclient
|
|
21
|
-
self.token = self.get_token()
|
|
22
|
-
|
|
23
|
-
def get_token(self) -> TokenModel:
|
|
24
|
-
if (
|
|
25
|
-
not hasattr(self, "token")
|
|
26
|
-
or self.token.expires_on is None
|
|
27
|
-
or self.token.expires_on < datetime.datetime.now()
|
|
28
|
-
):
|
|
29
|
-
url = f"{self.authclient.tokenEndpoint}"
|
|
30
|
-
data = {
|
|
31
|
-
"grant_type": "client_credentials",
|
|
32
|
-
"client_id": self.authclient.clientId,
|
|
33
|
-
"client_secret": self.authclient.clientSecret,
|
|
34
|
-
"scope": self.authclient.scope,
|
|
35
|
-
}
|
|
36
|
-
response = requests.post(url, data=data)
|
|
37
|
-
response.raise_for_status()
|
|
38
|
-
result = response.json()
|
|
39
|
-
self.token = TokenModel(
|
|
40
|
-
access_token=result["access_token"],
|
|
41
|
-
expires_on=datetime.datetime.now()
|
|
42
|
-
+ datetime.timedelta(seconds=result["expires_in"]),
|
|
43
|
-
)
|
|
44
|
-
logger.debug(f"Token: {self.token}")
|
|
45
|
-
return self.token
|
|
46
|
-
else:
|
|
47
|
-
return self.token
|
|
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
|
|
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
|
{sws_api_client-3.2.0 → sws_api_client-3.3.0b1}/sws_api_client/schemas/validation-report.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|