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.
Files changed (66) hide show
  1. aiod/__init__.py +61 -0
  2. aiod/_get.py +9 -0
  3. aiod/_user.py +0 -0
  4. aiod/authentication/__init__.py +10 -0
  5. aiod/authentication/authentication.py +370 -0
  6. aiod/base/__init__.py +5 -0
  7. aiod/base/_base_pkg.py +115 -0
  8. aiod/bookmarks.py +119 -0
  9. aiod/calls/__init__.py +0 -0
  10. aiod/calls/calls.py +577 -0
  11. aiod/calls/urls.py +93 -0
  12. aiod/calls/utils.py +62 -0
  13. aiod/configuration/__init__.py +1 -0
  14. aiod/configuration/_config.py +143 -0
  15. aiod/default/counts.py +38 -0
  16. aiod/models/__init__.py +5 -0
  17. aiod/models/_registry/__init__.py +5 -0
  18. aiod/models/_registry/_cls_lookup.py +67 -0
  19. aiod/models/_registry/_craft.py +172 -0
  20. aiod/models/_registry/_get.py +25 -0
  21. aiod/models/apis/__init__.py +5 -0
  22. aiod/models/apis/_sklearn_apis.py +25 -0
  23. aiod/models/base/__init__.py +5 -0
  24. aiod/models/base/_base.py +64 -0
  25. aiod/models/sklearn_apis/__init__.py +1 -0
  26. aiod/models/sklearn_apis/auto_sklearn.py +13 -0
  27. aiod/models/sklearn_apis/catboost.py +33 -0
  28. aiod/models/sklearn_apis/feature_engine.py +234 -0
  29. aiod/models/sklearn_apis/imbalanced_learn.py +109 -0
  30. aiod/models/sklearn_apis/lightgbm.py +42 -0
  31. aiod/models/sklearn_apis/mlxtend.py +64 -0
  32. aiod/models/sklearn_apis/scikit_learn.py +706 -0
  33. aiod/models/sklearn_apis/scikit_lego.py +195 -0
  34. aiod/models/sklearn_apis/skpro.py +220 -0
  35. aiod/models/sklearn_apis/xgboost.py +13 -0
  36. aiod/models/tests/__init__.py +1 -0
  37. aiod/models/tests/test_craft.py +62 -0
  38. aiod/models/tests/test_get.py +37 -0
  39. aiod/resources/__init__.py +0 -0
  40. aiod/resources/case_studies.py +15 -0
  41. aiod/resources/computational_assets.py +15 -0
  42. aiod/resources/contacts.py +15 -0
  43. aiod/resources/datasets.py +17 -0
  44. aiod/resources/educational_resources.py +15 -0
  45. aiod/resources/events.py +17 -0
  46. aiod/resources/experiments.py +17 -0
  47. aiod/resources/ml_models.py +17 -0
  48. aiod/resources/news.py +17 -0
  49. aiod/resources/organisations.py +17 -0
  50. aiod/resources/persons.py +15 -0
  51. aiod/resources/platforms.py +15 -0
  52. aiod/resources/projects.py +17 -0
  53. aiod/resources/publications.py +17 -0
  54. aiod/resources/services.py +17 -0
  55. aiod/resources/teams.py +15 -0
  56. aiod/taxonomies/__init__.py +158 -0
  57. aiod/utils/__init__.py +1 -0
  58. aiod/utils/_indexing/__init__.py +1 -0
  59. aiod/utils/_indexing/_preindex_sklearn.py +241 -0
  60. aiod/utils/_inmemory/__init__.py +1 -0
  61. aiod/utils/_inmemory/_dict.py +56 -0
  62. aiondemand-0.3.0.dist-info/METADATA +186 -0
  63. aiondemand-0.3.0.dist-info/RECORD +66 -0
  64. aiondemand-0.3.0.dist-info/WHEEL +4 -0
  65. aiondemand-0.3.0.dist-info/entry_points.txt +2 -0
  66. 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
@@ -0,0 +1,9 @@
1
+ """Global get dispatch utility."""
2
+
3
+ # currently just a forward to models
4
+ # to discuss and possibly
5
+ # todo: add global get utility here
6
+ # in general, e.g., datasets will not have same name as models etc
7
+ from aiod.models import get
8
+
9
+ __all__ = ["get"]
aiod/_user.py ADDED
File without changes
@@ -0,0 +1,10 @@
1
+ from .authentication import (
2
+ AuthenticationError,
3
+ NotAuthenticatedError,
4
+ Token,
5
+ create_token,
6
+ get_current_user,
7
+ get_token,
8
+ invalidate_token,
9
+ set_token,
10
+ )
@@ -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
@@ -0,0 +1,5 @@
1
+ """Module of base classes."""
2
+
3
+ from aiod.base._base_pkg import _BasePkg
4
+
5
+ __all__ = ["_BasePkg"]
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