aiondemand 0.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.
- aiod/__init__.py +61 -0
- aiod/_get.py +9 -0
- aiod/_user.py +0 -0
- aiod/authentication/__init__.py +10 -0
- aiod/authentication/authentication.py +370 -0
- aiod/base/__init__.py +5 -0
- aiod/base/_base_pkg.py +115 -0
- aiod/bookmarks.py +119 -0
- aiod/calls/__init__.py +0 -0
- aiod/calls/calls.py +577 -0
- aiod/calls/urls.py +93 -0
- aiod/calls/utils.py +62 -0
- aiod/configuration/__init__.py +1 -0
- aiod/configuration/_config.py +143 -0
- aiod/default/counts.py +38 -0
- aiod/models/__init__.py +5 -0
- aiod/models/_registry/__init__.py +5 -0
- aiod/models/_registry/_cls_lookup.py +67 -0
- aiod/models/_registry/_craft.py +172 -0
- aiod/models/_registry/_get.py +25 -0
- aiod/models/apis/__init__.py +5 -0
- aiod/models/apis/_sklearn_apis.py +25 -0
- aiod/models/base/__init__.py +5 -0
- aiod/models/base/_base.py +64 -0
- aiod/models/sklearn_apis/__init__.py +1 -0
- aiod/models/sklearn_apis/auto_sklearn.py +13 -0
- aiod/models/sklearn_apis/catboost.py +33 -0
- aiod/models/sklearn_apis/feature_engine.py +234 -0
- aiod/models/sklearn_apis/imbalanced_learn.py +109 -0
- aiod/models/sklearn_apis/lightgbm.py +42 -0
- aiod/models/sklearn_apis/mlxtend.py +64 -0
- aiod/models/sklearn_apis/scikit_learn.py +706 -0
- aiod/models/sklearn_apis/scikit_lego.py +195 -0
- aiod/models/sklearn_apis/skpro.py +220 -0
- aiod/models/sklearn_apis/xgboost.py +13 -0
- aiod/models/tests/__init__.py +1 -0
- aiod/models/tests/test_craft.py +62 -0
- aiod/models/tests/test_get.py +37 -0
- aiod/resources/__init__.py +0 -0
- aiod/resources/case_studies.py +15 -0
- aiod/resources/computational_assets.py +15 -0
- aiod/resources/contacts.py +15 -0
- aiod/resources/datasets.py +17 -0
- aiod/resources/educational_resources.py +15 -0
- aiod/resources/events.py +17 -0
- aiod/resources/experiments.py +17 -0
- aiod/resources/ml_models.py +17 -0
- aiod/resources/news.py +17 -0
- aiod/resources/organisations.py +17 -0
- aiod/resources/persons.py +15 -0
- aiod/resources/platforms.py +15 -0
- aiod/resources/projects.py +17 -0
- aiod/resources/publications.py +17 -0
- aiod/resources/services.py +17 -0
- aiod/resources/teams.py +15 -0
- aiod/taxonomies/__init__.py +158 -0
- aiod/utils/__init__.py +1 -0
- aiod/utils/_indexing/__init__.py +1 -0
- aiod/utils/_indexing/_preindex_sklearn.py +241 -0
- aiod/utils/_inmemory/__init__.py +1 -0
- aiod/utils/_inmemory/_dict.py +56 -0
- aiondemand-0.3.0.dist-info/METADATA +186 -0
- aiondemand-0.3.0.dist-info/RECORD +66 -0
- aiondemand-0.3.0.dist-info/WHEEL +4 -0
- aiondemand-0.3.0.dist-info/entry_points.txt +2 -0
- aiondemand-0.3.0.dist-info/licenses/LICENSE +22 -0
aiod/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from aiod import bookmarks, taxonomies
|
|
2
|
+
from aiod.authentication import create_token, get_current_user, invalidate_token
|
|
3
|
+
|
|
4
|
+
# from aiod.calls.calls import get_any_asset as get
|
|
5
|
+
from aiod.configuration import config
|
|
6
|
+
from aiod.default.counts import asset_counts as counts
|
|
7
|
+
from aiod.models import get
|
|
8
|
+
from aiod.resources import (
|
|
9
|
+
case_studies,
|
|
10
|
+
computational_assets,
|
|
11
|
+
contacts,
|
|
12
|
+
datasets,
|
|
13
|
+
educational_resources,
|
|
14
|
+
events,
|
|
15
|
+
experiments,
|
|
16
|
+
ml_models,
|
|
17
|
+
news,
|
|
18
|
+
organisations,
|
|
19
|
+
persons,
|
|
20
|
+
platforms,
|
|
21
|
+
projects,
|
|
22
|
+
publications,
|
|
23
|
+
services,
|
|
24
|
+
teams,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"config",
|
|
29
|
+
"taxonomies",
|
|
30
|
+
"bookmarks",
|
|
31
|
+
"case_studies",
|
|
32
|
+
"computational_assets",
|
|
33
|
+
"contacts",
|
|
34
|
+
"datasets",
|
|
35
|
+
"educational_resources",
|
|
36
|
+
"events",
|
|
37
|
+
"experiments",
|
|
38
|
+
"ml_models",
|
|
39
|
+
"news",
|
|
40
|
+
"organisations",
|
|
41
|
+
"persons",
|
|
42
|
+
"platforms",
|
|
43
|
+
"projects",
|
|
44
|
+
"publications",
|
|
45
|
+
"services",
|
|
46
|
+
"teams",
|
|
47
|
+
"invalidate_token",
|
|
48
|
+
"get_current_user",
|
|
49
|
+
"create_token",
|
|
50
|
+
"counts",
|
|
51
|
+
"get",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
# def __getattr__(name: str):
|
|
55
|
+
# if name in __all__:
|
|
56
|
+
# return globals()[name]
|
|
57
|
+
# if name not in __all__:
|
|
58
|
+
# return get(name)
|
|
59
|
+
# return None
|
|
60
|
+
|
|
61
|
+
__version__ = "0.3.0"
|
aiod/_get.py
ADDED
aiod/_user.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import http.client
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from http import HTTPStatus
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import NamedTuple
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
import tomlkit
|
|
13
|
+
from keycloak import KeycloakConnectionError, KeycloakOpenID, KeycloakPostError
|
|
14
|
+
|
|
15
|
+
from aiod.configuration import config
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
_token: "Token | None" = None
|
|
20
|
+
_user_token_file = Path("~/.aiod/token.toml").expanduser()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@functools.cache
|
|
24
|
+
def keycloak_openid() -> KeycloakOpenID:
|
|
25
|
+
secret = (_token._client_secret or None) if _token else None
|
|
26
|
+
return KeycloakOpenID(
|
|
27
|
+
server_url=config.auth_server,
|
|
28
|
+
client_id=config.client_id,
|
|
29
|
+
realm_name=config.realm,
|
|
30
|
+
client_secret_key=secret,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _on_keycloak_config_changed(_: str, __: str, ___: str) -> None:
|
|
35
|
+
keycloak_openid.cache_clear()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
config.subscribe("auth_server", on_change=_on_keycloak_config_changed)
|
|
39
|
+
config.subscribe("realm", on_change=_on_keycloak_config_changed)
|
|
40
|
+
config.subscribe("client_id", on_change=_on_keycloak_config_changed)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _datetime_utc_in(*, seconds: int) -> datetime:
|
|
44
|
+
span = timedelta(seconds=seconds)
|
|
45
|
+
return datetime.now(timezone.utc) + span
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Token:
|
|
49
|
+
"""Ensures active access tokens provided through one dedicated refresh token."""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
client_secret: str | None = None,
|
|
55
|
+
refresh_token: str | None = None,
|
|
56
|
+
access_token: str | None = None,
|
|
57
|
+
expires_in_seconds: int = -1,
|
|
58
|
+
):
|
|
59
|
+
if (client_secret and refresh_token) or not (client_secret or refresh_token):
|
|
60
|
+
raise ValueError("Must set exactly one of `client_secret` or `refresh_token`.")
|
|
61
|
+
if expires_in_seconds > 0 and access_token is None:
|
|
62
|
+
raise ValueError("If `expires_in_seconds` is set, `access_token` must be set to a valid access_token")
|
|
63
|
+
self._client_secret = client_secret
|
|
64
|
+
self._refresh_token = refresh_token
|
|
65
|
+
self._access_token = access_token or ""
|
|
66
|
+
self._expiration_date = _datetime_utc_in(seconds=expires_in_seconds)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def has_expired(self) -> bool:
|
|
70
|
+
"""Return whether the *access token* has expired based on local data."""
|
|
71
|
+
return datetime.now(timezone.utc) >= self._expiration_date
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def headers(self) -> dict[str, str]:
|
|
75
|
+
"""HTTP authorization header data for the token.
|
|
76
|
+
|
|
77
|
+
Examples
|
|
78
|
+
--------
|
|
79
|
+
```python
|
|
80
|
+
import aiod
|
|
81
|
+
token = aiod.get_token()
|
|
82
|
+
requests.post(url, headers=token.headers, json=metadata)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
"""
|
|
86
|
+
if self.has_expired:
|
|
87
|
+
self._refresh()
|
|
88
|
+
return {"Authorization": f"Bearer {self._access_token}"}
|
|
89
|
+
|
|
90
|
+
def __str__(self):
|
|
91
|
+
return self._refresh_token
|
|
92
|
+
|
|
93
|
+
def _refresh(self) -> None:
|
|
94
|
+
"""Use the `refresh token` or `client_secret` to request a new `access token`."""
|
|
95
|
+
try:
|
|
96
|
+
if self._refresh_token:
|
|
97
|
+
token_info = keycloak_openid().refresh_token(self._refresh_token)
|
|
98
|
+
if self._client_secret:
|
|
99
|
+
token_info = keycloak_openid().token(grant_type="client_credentials")
|
|
100
|
+
except KeycloakPostError:
|
|
101
|
+
raise AuthenticationError("Refresh token is not valid. Use `aiod.create_token` to get a new one.") from None
|
|
102
|
+
except KeycloakConnectionError as e:
|
|
103
|
+
e.add_note(f"Could not connect {config.auth_server!r}, try again later.")
|
|
104
|
+
raise
|
|
105
|
+
|
|
106
|
+
self._access_token = token_info["access_token"]
|
|
107
|
+
# self._refresh_token = token_info["refresh_token"] # Only with auto-rotating
|
|
108
|
+
# Because of the minuscule time difference between the server sending the
|
|
109
|
+
# response and us processing it, the `expires_in` may not be used directly
|
|
110
|
+
# when calculating expiration time.
|
|
111
|
+
SAFETY_PERIOD_SECONDS = 2 # noqa: N806
|
|
112
|
+
self._expiration_date = _datetime_utc_in(seconds=token_info["expires_in"] - SAFETY_PERIOD_SECONDS)
|
|
113
|
+
logger.info(f"Renewed access token, it expires {self._expiration_date}.")
|
|
114
|
+
|
|
115
|
+
def to_file(self, file: Path | None = None):
|
|
116
|
+
doc = tomlkit.document()
|
|
117
|
+
if self._refresh_token:
|
|
118
|
+
doc.add("refresh_token", self._refresh_token)
|
|
119
|
+
if self._client_secret:
|
|
120
|
+
doc.add("client_secret", self._client_secret)
|
|
121
|
+
if not self.has_expired:
|
|
122
|
+
doc.add("access_token", self._access_token)
|
|
123
|
+
doc.add("expiration_date", self._expiration_date.isoformat())
|
|
124
|
+
|
|
125
|
+
file = file or _user_token_file
|
|
126
|
+
file.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
file.write_text(tomlkit.dumps(doc))
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_file(cls, file: Path | None = None) -> "Token":
|
|
131
|
+
file = file or _user_token_file
|
|
132
|
+
doc = tomlkit.parse(file.read_text())
|
|
133
|
+
kwargs: dict[str, str | int] = {
|
|
134
|
+
"refresh_token": str(doc.get("refresh_token", "")),
|
|
135
|
+
"client_secret": str(doc.get("client_secret", "")),
|
|
136
|
+
}
|
|
137
|
+
if "expiration_date" in doc:
|
|
138
|
+
expiration_date = datetime.fromisoformat(doc["expiration_date"])
|
|
139
|
+
expires_in = expiration_date - _datetime_utc_in(seconds=0)
|
|
140
|
+
if expires_in.total_seconds() > 0:
|
|
141
|
+
kwargs.update(
|
|
142
|
+
{
|
|
143
|
+
"access_token": str(doc["access_token"]),
|
|
144
|
+
"expires_in_seconds": expires_in.seconds,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
return Token(**kwargs) # type: ignore[arg-type]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_token(token: Token) -> None:
|
|
151
|
+
"""Set the token directly.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
token
|
|
156
|
+
Sets the token to be used for authenticated requests.
|
|
157
|
+
|
|
158
|
+
Notes
|
|
159
|
+
-----
|
|
160
|
+
This function does not validate the provided token.
|
|
161
|
+
If the token is invalid, subsequent authenticated requests may fail.
|
|
162
|
+
"""
|
|
163
|
+
global _token
|
|
164
|
+
_token = token
|
|
165
|
+
keycloak_openid.cache_clear()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_token() -> Token:
|
|
169
|
+
"""Get the currently configured token that is used for authenticated requests.
|
|
170
|
+
|
|
171
|
+
Returns
|
|
172
|
+
-------
|
|
173
|
+
:
|
|
174
|
+
"""
|
|
175
|
+
if _token is None:
|
|
176
|
+
msg = "No token set. Please create a new token with `aiod.create_token()`, or set one with `aiod.set_token('...')`."
|
|
177
|
+
raise NotAuthenticatedError(msg)
|
|
178
|
+
return _token
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _get_auth_headers(*, required: bool = True) -> dict:
|
|
182
|
+
try:
|
|
183
|
+
return get_token().headers
|
|
184
|
+
except (AuthenticationError, NotAuthenticatedError):
|
|
185
|
+
if required:
|
|
186
|
+
raise
|
|
187
|
+
return {}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def create_token(
|
|
191
|
+
timeout_seconds: int = 300,
|
|
192
|
+
*,
|
|
193
|
+
write_to_file: bool = False,
|
|
194
|
+
use_in_requests: bool = True,
|
|
195
|
+
) -> Token:
|
|
196
|
+
"""Get an API Key by prompting the user to log in through a browser.
|
|
197
|
+
|
|
198
|
+
Notes
|
|
199
|
+
-----
|
|
200
|
+
This is a blocking function, and will poll the authentication server until
|
|
201
|
+
authentication is completed or `timeout_seconds` have passed.
|
|
202
|
+
|
|
203
|
+
Parameters
|
|
204
|
+
----------
|
|
205
|
+
timeout_seconds
|
|
206
|
+
The maximum time this function blocks waiting for the authentication workflow
|
|
207
|
+
to complete. If `timeout_seconds` seconds have elapsed without successful
|
|
208
|
+
authentication, this function raises an AuthenticationError.
|
|
209
|
+
This must be set to a positive integer.
|
|
210
|
+
write_to_file
|
|
211
|
+
If set to true, the new api key (refresh token) will automatically be saved to
|
|
212
|
+
the user configuration file (~/.aiod/config.toml).
|
|
213
|
+
use_in_requests
|
|
214
|
+
If set to true, the new token will automatically be used for future authenticated
|
|
215
|
+
requests.
|
|
216
|
+
|
|
217
|
+
Returns
|
|
218
|
+
-------
|
|
219
|
+
:
|
|
220
|
+
The new token for use in authenticated requests.
|
|
221
|
+
|
|
222
|
+
Raises
|
|
223
|
+
------
|
|
224
|
+
AuthenticationError
|
|
225
|
+
If authentication is unsuccessful in any way.
|
|
226
|
+
|
|
227
|
+
"""
|
|
228
|
+
if timeout_seconds <= 0 or not isinstance(timeout_seconds, int):
|
|
229
|
+
raise ValueError("`timeout_seconds` must be a positive integer.")
|
|
230
|
+
kc = keycloak_openid()
|
|
231
|
+
|
|
232
|
+
response = kc.device()
|
|
233
|
+
device_code = response["device_code"]
|
|
234
|
+
user_code = response["user_code"]
|
|
235
|
+
verification_uri = response["verification_uri"]
|
|
236
|
+
verification_uri_complete = response["verification_uri_complete"]
|
|
237
|
+
|
|
238
|
+
# We print instead of log because this information *needs* to get to the user,
|
|
239
|
+
# and relying on user logging configurations is too finnicky for that.
|
|
240
|
+
print("Please authenticate using one of two methods:") # noqa: T201
|
|
241
|
+
print() # noqa: T201
|
|
242
|
+
print(f" 1. Navigate to {verification_uri_complete}") # noqa: T201
|
|
243
|
+
print( # noqa: T201
|
|
244
|
+
f" 2. Navigate to {verification_uri} and enter code {user_code}"
|
|
245
|
+
)
|
|
246
|
+
print() # noqa: T201
|
|
247
|
+
print( # noqa: T201
|
|
248
|
+
f"This workflow will automatically abort after {timeout_seconds} seconds."
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
poll_interval = response["interval"]
|
|
252
|
+
start_time = time.time()
|
|
253
|
+
|
|
254
|
+
token_endpoint = kc.well_known()["token_endpoint"]
|
|
255
|
+
token_data = {
|
|
256
|
+
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
|
|
257
|
+
"client_id": config.client_id,
|
|
258
|
+
"device_code": device_code,
|
|
259
|
+
}
|
|
260
|
+
# We do not know when the user finishes their authentication, so we poll the server
|
|
261
|
+
while time.time() - start_time < timeout_seconds:
|
|
262
|
+
time.sleep(poll_interval)
|
|
263
|
+
token_response = requests.post(
|
|
264
|
+
token_endpoint,
|
|
265
|
+
data=token_data,
|
|
266
|
+
timeout=config.request_timeout_seconds,
|
|
267
|
+
)
|
|
268
|
+
token_response_data = token_response.json()
|
|
269
|
+
|
|
270
|
+
response = (token_response.status_code, token_response_data.get("error"))
|
|
271
|
+
match response:
|
|
272
|
+
case (HTTPStatus.OK, _):
|
|
273
|
+
access_token = token_response_data["access_token"]
|
|
274
|
+
kc.decode_token(access_token, validate=True)
|
|
275
|
+
token = Token(
|
|
276
|
+
refresh_token=token_response_data["refresh_token"],
|
|
277
|
+
access_token=access_token,
|
|
278
|
+
expires_in_seconds=token_response_data["expires_in"],
|
|
279
|
+
)
|
|
280
|
+
if write_to_file:
|
|
281
|
+
token.to_file(_user_token_file)
|
|
282
|
+
if use_in_requests:
|
|
283
|
+
set_token(token)
|
|
284
|
+
return token
|
|
285
|
+
case (HTTPStatus.BAD_REQUEST, "authorization_pending"):
|
|
286
|
+
continue
|
|
287
|
+
case (HTTPStatus.BAD_REQUEST, "slow_down"):
|
|
288
|
+
poll_interval *= 1.5
|
|
289
|
+
continue
|
|
290
|
+
case (HTTPStatus.BAD_REQUEST, "access_denied"):
|
|
291
|
+
raise AuthenticationError("Access denied by Keycloak server.")
|
|
292
|
+
case (HTTPStatus.BAD_REQUEST, "expired_token"):
|
|
293
|
+
raise AuthenticationError("Device code has expired, please try again.")
|
|
294
|
+
case (status, error):
|
|
295
|
+
raise AuthenticationError(f"Unexpected error, please contact the developers ({status}, {error}).")
|
|
296
|
+
raise AuthenticationError(f"No successful authentication within {timeout_seconds=} seconds.")
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def invalidate_token(token: str | Token | None = None, ignore_errors: bool = False) -> None:
|
|
300
|
+
"""Invalidates the current (or provided) API key.
|
|
301
|
+
|
|
302
|
+
Ends the current keycloak session, invalidating all keys issued.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
token
|
|
307
|
+
The token to invalidate.
|
|
308
|
+
If str, it should be a refresh token.
|
|
309
|
+
If None, it will default to the currently configured token.
|
|
310
|
+
ignore_errors
|
|
311
|
+
If true, do not raise an error if the logout attempt failed.
|
|
312
|
+
"""
|
|
313
|
+
global _token
|
|
314
|
+
token = token or _token
|
|
315
|
+
try:
|
|
316
|
+
keycloak_openid().logout(token)
|
|
317
|
+
except (KeycloakPostError, KeycloakConnectionError) as e:
|
|
318
|
+
if not ignore_errors:
|
|
319
|
+
raise e
|
|
320
|
+
finally:
|
|
321
|
+
_token = None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class User(NamedTuple):
|
|
325
|
+
name: str
|
|
326
|
+
roles: Sequence[str]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def get_current_user() -> User:
|
|
330
|
+
"""Return name and roles of the user that is currently authenticated.
|
|
331
|
+
|
|
332
|
+
Returns
|
|
333
|
+
-------
|
|
334
|
+
User
|
|
335
|
+
The user information for the currently authenticated user.
|
|
336
|
+
|
|
337
|
+
Raises
|
|
338
|
+
------
|
|
339
|
+
NotAuthenticatedError
|
|
340
|
+
When the user is not authenticated.
|
|
341
|
+
"""
|
|
342
|
+
response = requests.get(
|
|
343
|
+
f"{config.api_server}authorization_test",
|
|
344
|
+
headers=get_token().headers,
|
|
345
|
+
timeout=config.request_timeout_seconds,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
content = response.json()
|
|
349
|
+
if response.status_code == http.client.UNAUTHORIZED:
|
|
350
|
+
raise NotAuthenticatedError(content)
|
|
351
|
+
return User(
|
|
352
|
+
name=content["name"],
|
|
353
|
+
roles=tuple(content["roles"]),
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class AuthenticationError(Exception):
|
|
358
|
+
"""Raised when an authentication error occurred."""
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
class NotAuthenticatedError(Exception):
|
|
362
|
+
"""Raised when an endpoint that requires authentication is called without authentication."""
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
if _user_token_file.exists() and _user_token_file.is_file():
|
|
366
|
+
try:
|
|
367
|
+
_token = Token.from_file(_user_token_file)
|
|
368
|
+
except Exception as e:
|
|
369
|
+
e.add_note(f"Failed to load credentials from {str(_user_token_file)!r}")
|
|
370
|
+
raise e
|
aiod/base/__init__.py
ADDED
aiod/base/_base_pkg.py
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Base Packager class."""
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from skbase.base import BaseObject
|
|
9
|
+
from skbase.utils.dependencies import _check_estimator_deps
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class _BasePkg(BaseObject):
|
|
13
|
+
_tags = {
|
|
14
|
+
"python_dependencies": None,
|
|
15
|
+
"python_version": None,
|
|
16
|
+
# package register and manifest
|
|
17
|
+
"pkg_id": None, # object id contained, "__multiple" if multiple
|
|
18
|
+
"pkg_obj": "reference", # or "code"
|
|
19
|
+
"pkg_obj_type": None, # API contract type
|
|
20
|
+
"pkg_compression": "zlib", # compression
|
|
21
|
+
"pkg_pypi_name": None, # PyPI package name of objects
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
def materialize(self):
|
|
28
|
+
try:
|
|
29
|
+
_check_estimator_deps(obj=self)
|
|
30
|
+
except ModuleNotFoundError as e:
|
|
31
|
+
# prettier message, so the reference is to the pkg_id
|
|
32
|
+
# currently, we cannot simply pass the object name to skbase
|
|
33
|
+
# in the error message, so this is a hack
|
|
34
|
+
# todo: fix this in scikit-base
|
|
35
|
+
msg = str(e)
|
|
36
|
+
if len(msg) > 11:
|
|
37
|
+
msg = msg[11:]
|
|
38
|
+
raise ModuleNotFoundError(msg) from e
|
|
39
|
+
|
|
40
|
+
return self._materialize()
|
|
41
|
+
|
|
42
|
+
def _materialize(self):
|
|
43
|
+
raise RuntimeError("abstract method")
|
|
44
|
+
|
|
45
|
+
def serialize(self):
|
|
46
|
+
cls_str = class_to_source(type(self))
|
|
47
|
+
compress_method = self.get_tag("pkg_compression")
|
|
48
|
+
if compress_method in [None, "None"]:
|
|
49
|
+
return cls_str
|
|
50
|
+
|
|
51
|
+
cls_str = cls_str.encode("utf-8")
|
|
52
|
+
exec(f"import {compress_method}")
|
|
53
|
+
return eval(f"{compress_method}.compress(cls_str)")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _has_source(obj) -> bool:
|
|
57
|
+
"""Return True if inspect.getsource(obj) should succeed."""
|
|
58
|
+
module_name = getattr(obj, "__module__", None)
|
|
59
|
+
if not module_name or module_name not in sys.modules:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
module = sys.modules[module_name]
|
|
63
|
+
file = getattr(module, "__file__", None)
|
|
64
|
+
if not file:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return Path(file).suffix == ".py"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def class_to_source(cls) -> str:
|
|
71
|
+
"""Return full source definition of python class as string.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
cls : class to serialize
|
|
76
|
+
|
|
77
|
+
Returns
|
|
78
|
+
-------
|
|
79
|
+
str : complete definition of cls, as str.
|
|
80
|
+
Imports are not contained or serialized.
|
|
81
|
+
"""
|
|
82
|
+
# Fast path: class has retrievable source
|
|
83
|
+
if _has_source(cls):
|
|
84
|
+
source = inspect.getsource(cls)
|
|
85
|
+
return textwrap.dedent(source)
|
|
86
|
+
|
|
87
|
+
# Fallback for dynamically created classes
|
|
88
|
+
lines = []
|
|
89
|
+
|
|
90
|
+
bases = [base.__name__ for base in cls.__bases__ if base is not object]
|
|
91
|
+
base_str = f"({', '.join(bases)})" if bases else ""
|
|
92
|
+
lines.append(f"class {cls.__name__}{base_str}:")
|
|
93
|
+
|
|
94
|
+
body_added = False
|
|
95
|
+
|
|
96
|
+
for name, value in cls.__dict__.items():
|
|
97
|
+
if name.startswith("__") and name.endswith("__"):
|
|
98
|
+
continue
|
|
99
|
+
|
|
100
|
+
if inspect.isfunction(value):
|
|
101
|
+
if _has_source(value):
|
|
102
|
+
method_src = inspect.getsource(value)
|
|
103
|
+
method_src = textwrap.indent(textwrap.dedent(method_src), " ")
|
|
104
|
+
lines.append(method_src)
|
|
105
|
+
else:
|
|
106
|
+
lines.append(f" def {name}(self): ...")
|
|
107
|
+
body_added = True
|
|
108
|
+
else:
|
|
109
|
+
lines.append(f" {name} = {value!r}")
|
|
110
|
+
body_added = True
|
|
111
|
+
|
|
112
|
+
if not body_added:
|
|
113
|
+
lines.append(" pass")
|
|
114
|
+
|
|
115
|
+
return "\n".join(lines)
|
aiod/bookmarks.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
from aiod.authentication import get_token
|
|
8
|
+
from aiod.calls.urls import server_url
|
|
9
|
+
from aiod.calls.utils import ServerError
|
|
10
|
+
from aiod.configuration import config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Bookmark:
|
|
15
|
+
"""Reference to an asset on AI-on-Demand.
|
|
16
|
+
|
|
17
|
+
Attributes
|
|
18
|
+
----------
|
|
19
|
+
identifier: str
|
|
20
|
+
The identifier of the asset on AI-on-Demand, e.g., 'data_xyz...'
|
|
21
|
+
created: datetime
|
|
22
|
+
The datetime when the bookmark was originally created.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
identifier: str
|
|
26
|
+
created: datetime
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _bookmarks_url() -> str:
|
|
30
|
+
return server_url() + "bookmarks"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def register(identifier: str) -> Bookmark:
|
|
34
|
+
"""Bookmark the asset with `identifier` for the user.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
identifier:
|
|
39
|
+
The identifier of the asset on AI-on-Demand, e.g., 'data_xyz...'
|
|
40
|
+
|
|
41
|
+
Returns
|
|
42
|
+
-------
|
|
43
|
+
:
|
|
44
|
+
A `Bookmark` that denotes the time the bookmark was created.
|
|
45
|
+
|
|
46
|
+
Raises
|
|
47
|
+
------
|
|
48
|
+
KeyError
|
|
49
|
+
If the identifier is not recognized by AI-on-Demand.
|
|
50
|
+
ServerError
|
|
51
|
+
If any other server-side error occurs.
|
|
52
|
+
"""
|
|
53
|
+
res = requests.post(
|
|
54
|
+
_bookmarks_url(),
|
|
55
|
+
params={"resource_identifier": identifier},
|
|
56
|
+
headers=get_token().headers,
|
|
57
|
+
timeout=config.request_timeout_seconds,
|
|
58
|
+
)
|
|
59
|
+
if res.status_code == HTTPStatus.NOT_FOUND:
|
|
60
|
+
raise KeyError(f"Could not find asset with identifier {identifier!r}.")
|
|
61
|
+
if res.status_code != HTTPStatus.OK:
|
|
62
|
+
raise ServerError(res)
|
|
63
|
+
|
|
64
|
+
return Bookmark(
|
|
65
|
+
identifier=res.json()["resource_identifier"],
|
|
66
|
+
created=datetime.fromisoformat(res.json()["created_at"]).replace(tzinfo=timezone.utc),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def delete(identifier: str):
|
|
71
|
+
"""Remove the bookmark for the asset with `identifier` for the user.
|
|
72
|
+
|
|
73
|
+
This method does not raise an error if the identifier is not recognized by AI-on-Demand,
|
|
74
|
+
or if the identifier is valid but the user has not bookmarked it.
|
|
75
|
+
|
|
76
|
+
Parameters
|
|
77
|
+
----------
|
|
78
|
+
identifier:
|
|
79
|
+
The identifier of the asset on AI-on-Demand, e.g., 'data_xyz...'
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
Raises
|
|
83
|
+
------
|
|
84
|
+
ServerError
|
|
85
|
+
If any other server-side error occurs.
|
|
86
|
+
"""
|
|
87
|
+
res = requests.delete(
|
|
88
|
+
_bookmarks_url(),
|
|
89
|
+
params={"resource_identifier": identifier},
|
|
90
|
+
headers=get_token().headers,
|
|
91
|
+
timeout=config.request_timeout_seconds,
|
|
92
|
+
)
|
|
93
|
+
# The delete endpoint does not error if the bookmark does not exist
|
|
94
|
+
if res.status_code != HTTPStatus.OK:
|
|
95
|
+
raise ServerError(res)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def get_list() -> list[Bookmark]:
|
|
99
|
+
"""Return a list of the user's bookmarks.
|
|
100
|
+
|
|
101
|
+
Returns
|
|
102
|
+
-------
|
|
103
|
+
:
|
|
104
|
+
The list of bookmarks.
|
|
105
|
+
"""
|
|
106
|
+
res = requests.get(
|
|
107
|
+
_bookmarks_url(),
|
|
108
|
+
headers=get_token().headers,
|
|
109
|
+
timeout=config.request_timeout_seconds,
|
|
110
|
+
)
|
|
111
|
+
if res.status_code != HTTPStatus.OK:
|
|
112
|
+
raise ServerError(res)
|
|
113
|
+
return [
|
|
114
|
+
Bookmark(
|
|
115
|
+
identifier=bookmark["resource_identifier"],
|
|
116
|
+
created=datetime.fromisoformat(bookmark["created_at"]).replace(tzinfo=timezone.utc),
|
|
117
|
+
)
|
|
118
|
+
for bookmark in res.json()
|
|
119
|
+
]
|
aiod/calls/__init__.py
ADDED
|
File without changes
|