pyoncat 2.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.
- pyoncat/LICENSE +674 -0
- pyoncat/__init__.py +1066 -0
- pyoncat-2.0.dist-info/METADATA +749 -0
- pyoncat-2.0.dist-info/RECORD +5 -0
- pyoncat-2.0.dist-info/WHEEL +4 -0
pyoncat/__init__.py
ADDED
|
@@ -0,0 +1,1066 @@
|
|
|
1
|
+
r"""A Python Client for the API of ONCat, the ORNL Neutron Catalog.
|
|
2
|
+
|
|
3
|
+
This package provides several convenience classes and methods which will
|
|
4
|
+
help to more easily interact with the ONCat API from Python.
|
|
5
|
+
|
|
6
|
+
For a complete description of this module, please see the documentation
|
|
7
|
+
hosted at https://oncat.ornl.gov.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import contextlib
|
|
13
|
+
import functools
|
|
14
|
+
import getpass
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
import pathlib
|
|
18
|
+
import sys
|
|
19
|
+
import warnings
|
|
20
|
+
import weakref
|
|
21
|
+
from typing import Any, Callable, ClassVar
|
|
22
|
+
|
|
23
|
+
import oauthlib.oauth2
|
|
24
|
+
import requests
|
|
25
|
+
import requests_oauthlib
|
|
26
|
+
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
|
27
|
+
|
|
28
|
+
__version__ = "2.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"FileSystemTokenStore",
|
|
32
|
+
"ONCat",
|
|
33
|
+
"ONCatObject",
|
|
34
|
+
"PyONCatError",
|
|
35
|
+
"prompt_user_for_login",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
########################################################################
|
|
40
|
+
# Client
|
|
41
|
+
########################################################################
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
CLIENT_CREDENTIALS_FLOW = "client"
|
|
45
|
+
RESOURCE_OWNER_CREDENTIALS_FLOW = "resource_owner"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ONCat:
|
|
49
|
+
"""The ONCat API client.
|
|
50
|
+
|
|
51
|
+
This class is the main entry point for interacting with the ONCat
|
|
52
|
+
API. It aims to provide a slightly higher level of abstraction than
|
|
53
|
+
simply making raw GET, POST, PUT, and DELETE requests to the API,
|
|
54
|
+
and also wraps the relatively complex authentication logic that is
|
|
55
|
+
required to interact with most of the API.
|
|
56
|
+
|
|
57
|
+
Once instantiated correctly, the client object provides a set of
|
|
58
|
+
standardized sub-objects that represent the various resources that
|
|
59
|
+
are available in the API and the actions that can be taken on them.
|
|
60
|
+
|
|
61
|
+
Calling actions on a resource takes the following form:
|
|
62
|
+
|
|
63
|
+
client.Resource.action()
|
|
64
|
+
|
|
65
|
+
For example, to list all the facilities in the API, you would call:
|
|
66
|
+
|
|
67
|
+
client.Facility.list()
|
|
68
|
+
|
|
69
|
+
The client will automatically handle the authentication and
|
|
70
|
+
authorization required to make the request, and will raise an
|
|
71
|
+
exception if the request is not successful.
|
|
72
|
+
|
|
73
|
+
Various parameters can be passed to the actions to modify the
|
|
74
|
+
behaviour of the underlying request, such as query parameters, data
|
|
75
|
+
to be sent in the body of the request, etc.
|
|
76
|
+
|
|
77
|
+
A more complex example would be to return a list of each run title
|
|
78
|
+
contained in all the raw NeXus files acquired at ARCS:
|
|
79
|
+
|
|
80
|
+
oncat.Datafile.list(
|
|
81
|
+
facility="SNS",
|
|
82
|
+
instrument="ARCS",
|
|
83
|
+
projection=["metadata.entry.title"],
|
|
84
|
+
tags=["type/raw"],
|
|
85
|
+
exts=[".nxs.h5"],
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
A full explanation of each resource and the actions that can be
|
|
89
|
+
taken on them can be found in the ONCat API documentation.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
Arguments:
|
|
93
|
+
---------
|
|
94
|
+
url: str
|
|
95
|
+
The base URL of the ONCat API.
|
|
96
|
+
|
|
97
|
+
client_id: str | None
|
|
98
|
+
The client ID for the OAuth client, required if you are
|
|
99
|
+
using an OAuth flow to authenticate.
|
|
100
|
+
|
|
101
|
+
client_secret: str | None
|
|
102
|
+
The client secret for the OAuth client, which may be
|
|
103
|
+
required alongside the client ID if you are using one.
|
|
104
|
+
|
|
105
|
+
token_getter: Callable[[], str | None] | None
|
|
106
|
+
An optional function that will be used to load the current
|
|
107
|
+
OAuth token if one exists, else `None`.
|
|
108
|
+
|
|
109
|
+
token_setter: Callable[[str | None], None] | None
|
|
110
|
+
An optional function that will be used to save the current
|
|
111
|
+
OAuth token, to be retrieved later by the `token_getter`.
|
|
112
|
+
|
|
113
|
+
login_prompt: Callable[[], Tuple[str, str]] | None
|
|
114
|
+
An optional function that will prompt the user for their
|
|
115
|
+
username and password when appropriate, as per the OAuth
|
|
116
|
+
flow being used.
|
|
117
|
+
|
|
118
|
+
api_token: str | None
|
|
119
|
+
An optional API token that can be used by select clients to
|
|
120
|
+
authenticate requests to the API as an alternative to OAuth.
|
|
121
|
+
|
|
122
|
+
flow: CLIENT_CREDENTIALS_FLOW | RESOURCE_OWNER_CREDENTIALS_FLOW | None
|
|
123
|
+
The OAuth "flow" to use for authentication. Required if you
|
|
124
|
+
are using OAuth.
|
|
125
|
+
|
|
126
|
+
scopes: List[str] | None
|
|
127
|
+
The OAuth scopes that this client should have access to.
|
|
128
|
+
If using OAuth and a scope is not provided, then will
|
|
129
|
+
default server-side to `"api:read"`. Please see the ONCat
|
|
130
|
+
API documentation for more information on the scopes that
|
|
131
|
+
are required for each resource.
|
|
132
|
+
|
|
133
|
+
verify: bool
|
|
134
|
+
Whether or not to verify the SSL certificate of the server
|
|
135
|
+
when making requests. `True` by default and absolutely
|
|
136
|
+
required in production, setting this to `False` can be
|
|
137
|
+
useful for development or testing purposes.
|
|
138
|
+
|
|
139
|
+
timeout: int | None
|
|
140
|
+
The number of seconds to wait for a response from the server
|
|
141
|
+
before timing out. `None` by default, which means that the
|
|
142
|
+
client will wait indefinitely for a response, as per the
|
|
143
|
+
underlying behavior of the `requests` module.
|
|
144
|
+
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
url: str,
|
|
150
|
+
client_id: str | None = None,
|
|
151
|
+
client_secret: str | None = None,
|
|
152
|
+
token_getter: Callable[[], str | None] | None = None,
|
|
153
|
+
token_setter: Callable[[str | None], None] | None = None,
|
|
154
|
+
login_prompt: Callable[[], tuple[str, str]] | None = None,
|
|
155
|
+
api_token: str | None = None,
|
|
156
|
+
flow: CLIENT_CREDENTIALS_FLOW
|
|
157
|
+
| RESOURCE_OWNER_CREDENTIALS_FLOW
|
|
158
|
+
| None = None,
|
|
159
|
+
scopes: list[str] | None = None,
|
|
160
|
+
verify: bool = True,
|
|
161
|
+
timeout: int | None = None,
|
|
162
|
+
):
|
|
163
|
+
if not url:
|
|
164
|
+
msg = "A URL must be provided."
|
|
165
|
+
raise ValueError(msg)
|
|
166
|
+
|
|
167
|
+
using_oauth = (
|
|
168
|
+
client_id is not None
|
|
169
|
+
or client_secret is not None
|
|
170
|
+
or token_getter is not None
|
|
171
|
+
or token_setter is not None
|
|
172
|
+
or login_prompt is not None
|
|
173
|
+
or flow is not None
|
|
174
|
+
or scopes is not None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if using_oauth:
|
|
178
|
+
if not flow:
|
|
179
|
+
msg = "A flow must be provided when using OAuth."
|
|
180
|
+
raise ValueError(msg)
|
|
181
|
+
if not client_id:
|
|
182
|
+
msg = "A client ID must be provided when using OAuth."
|
|
183
|
+
raise ValueError(msg)
|
|
184
|
+
if api_token:
|
|
185
|
+
msg = "An API token cannot be used when using OAuth."
|
|
186
|
+
raise ValueError(msg)
|
|
187
|
+
if client_secret and not client_id:
|
|
188
|
+
msg = "A client secret must be paired with a client ID."
|
|
189
|
+
raise ValueError(msg)
|
|
190
|
+
if token_getter and not token_setter:
|
|
191
|
+
msg = "A token getter must be paired with a token setter."
|
|
192
|
+
raise ValueError(msg)
|
|
193
|
+
if token_setter and not token_getter:
|
|
194
|
+
msg = "A token setter must be paired with a token getter."
|
|
195
|
+
raise ValueError(msg)
|
|
196
|
+
|
|
197
|
+
self._token_getter = token_getter
|
|
198
|
+
self._token_setter = token_setter
|
|
199
|
+
self._login_prompt = login_prompt
|
|
200
|
+
self._api_token = api_token
|
|
201
|
+
self._client_id = client_id
|
|
202
|
+
self._client_secret = client_secret
|
|
203
|
+
self._url = url
|
|
204
|
+
self._flow = flow
|
|
205
|
+
self._scopes = scopes
|
|
206
|
+
self._verify = verify
|
|
207
|
+
self._timeout = timeout
|
|
208
|
+
|
|
209
|
+
self._token = None
|
|
210
|
+
self._oauth_client = None
|
|
211
|
+
|
|
212
|
+
for resource_cls in _ONCatMixinMeta.REGISTERED_RESOURCES:
|
|
213
|
+
setattr(self, resource_cls.__name__, resource_cls(self))
|
|
214
|
+
|
|
215
|
+
def _get(self, url: str, **kwargs):
|
|
216
|
+
return self._call_method("get", url, None, **kwargs)
|
|
217
|
+
|
|
218
|
+
def _put(self, url: str, data: Any, **kwargs):
|
|
219
|
+
result = self._call_method("put", url, data, **kwargs)
|
|
220
|
+
|
|
221
|
+
# Not all resources will return a confirmation representation.
|
|
222
|
+
return result if result != "" else None
|
|
223
|
+
|
|
224
|
+
def _post(self, url: str, data: Any, **kwargs):
|
|
225
|
+
return self._call_method("post", url, data, **kwargs)
|
|
226
|
+
|
|
227
|
+
def _delete(self, url: str, **kwargs):
|
|
228
|
+
self._call_method("delete", url, None, **kwargs)
|
|
229
|
+
|
|
230
|
+
def _call_method(self, method, url, data, **kwargs):
|
|
231
|
+
url = requests.utils.quote(url)
|
|
232
|
+
full_url = (
|
|
233
|
+
self._url + url if url.startswith("/") else self._url + "/" + url
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def send_request():
|
|
237
|
+
if self._client_id:
|
|
238
|
+
response = getattr(self._load_oauth_client(), method)(
|
|
239
|
+
full_url,
|
|
240
|
+
params=kwargs,
|
|
241
|
+
json=data,
|
|
242
|
+
verify=self._should_verify(),
|
|
243
|
+
timeout=self._timeout,
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
response = getattr(requests, method)(
|
|
247
|
+
full_url,
|
|
248
|
+
params=kwargs,
|
|
249
|
+
json=data,
|
|
250
|
+
verify=self._should_verify(),
|
|
251
|
+
headers={"Authorization": "Bearer " + self._api_token}
|
|
252
|
+
if self._api_token
|
|
253
|
+
else None,
|
|
254
|
+
timeout=self._timeout,
|
|
255
|
+
)
|
|
256
|
+
try:
|
|
257
|
+
response.raise_for_status()
|
|
258
|
+
except requests.exceptions.HTTPError as error:
|
|
259
|
+
if error.response.status_code == 400:
|
|
260
|
+
msg = "Bad request"
|
|
261
|
+
raise BadRequestError(msg) from error
|
|
262
|
+
if error.response.status_code == 401:
|
|
263
|
+
msg = f'Not authorized to access "{full_url}"'
|
|
264
|
+
raise UnauthorizedError(msg) from error
|
|
265
|
+
if error.response.status_code == 404:
|
|
266
|
+
msg = f'Could not find resource at "{full_url}"'
|
|
267
|
+
raise NotFoundError(msg) from error
|
|
268
|
+
msg = f'Error: "{error!s}"'
|
|
269
|
+
raise PyONCatError(msg) from error
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
return response.json()
|
|
273
|
+
except ValueError:
|
|
274
|
+
return response.text
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
return send_request()
|
|
278
|
+
except oauthlib.oauth2.rfc6749.errors.InvalidGrantError as error:
|
|
279
|
+
if (
|
|
280
|
+
self._flow == RESOURCE_OWNER_CREDENTIALS_FLOW
|
|
281
|
+
and "(invalid_grant)" in str(error)
|
|
282
|
+
and "unknown, invalid, or expired refresh token" in str(error)
|
|
283
|
+
):
|
|
284
|
+
self._save_token(None)
|
|
285
|
+
if self._login_prompt:
|
|
286
|
+
self.login()
|
|
287
|
+
return send_request()
|
|
288
|
+
msg = (
|
|
289
|
+
"It looks like you've tried to use a refresh token "
|
|
290
|
+
"that has expired. Not to worry -- this is part of "
|
|
291
|
+
"the normal OAuth workflow when using refresh tokens, "
|
|
292
|
+
"since by default (and on a client-by-client basis) "
|
|
293
|
+
"they are set to expire after a certain length of "
|
|
294
|
+
"time. You should be catching this error in your "
|
|
295
|
+
"client code and then re-prompting the user for their "
|
|
296
|
+
"username and password so that you may proceed with "
|
|
297
|
+
"your calls to the ONCat API.\n\n"
|
|
298
|
+
)
|
|
299
|
+
raise InvalidRefreshTokenError(msg) from error
|
|
300
|
+
msg = f'Error: "{error!s}"'
|
|
301
|
+
raise PyONCatError(msg) from error
|
|
302
|
+
except oauthlib.oauth2.TokenExpiredError as error:
|
|
303
|
+
if self._flow == CLIENT_CREDENTIALS_FLOW:
|
|
304
|
+
self.login()
|
|
305
|
+
return send_request()
|
|
306
|
+
msg = f'Error: "{error!s}"'
|
|
307
|
+
raise PyONCatError(msg) from error
|
|
308
|
+
|
|
309
|
+
def _load_oauth_client(self):
|
|
310
|
+
if not self._oauth_client:
|
|
311
|
+
self.login()
|
|
312
|
+
|
|
313
|
+
return self._oauth_client
|
|
314
|
+
|
|
315
|
+
def _load_token(self):
|
|
316
|
+
if self._token_getter is not None:
|
|
317
|
+
return self._token_getter()
|
|
318
|
+
|
|
319
|
+
return self._token
|
|
320
|
+
|
|
321
|
+
def _save_token(self, token):
|
|
322
|
+
if self._token_setter is not None:
|
|
323
|
+
self._token_setter(token)
|
|
324
|
+
else:
|
|
325
|
+
self._token = token
|
|
326
|
+
|
|
327
|
+
def _should_verify(self):
|
|
328
|
+
if not self._verify:
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
if (
|
|
332
|
+
"localhost" in self._url
|
|
333
|
+
or "load-balancer" in self._url
|
|
334
|
+
or "proxy" in self._url
|
|
335
|
+
):
|
|
336
|
+
# Ignore invalid certs and lack of SSL for OAuth if
|
|
337
|
+
# deploying locally.
|
|
338
|
+
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
|
|
339
|
+
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
|
340
|
+
|
|
341
|
+
return (
|
|
342
|
+
"localhost" not in self._url
|
|
343
|
+
and "load-balancer" not in self._url
|
|
344
|
+
and "proxy" not in self._url
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def login(self, username=None, password=None):
|
|
348
|
+
"""Login to the ONCat API as part of an appropriate OAuth flow.
|
|
349
|
+
|
|
350
|
+
This method is no longer strictly necessary to call manually,
|
|
351
|
+
since if you have specified a login prompt callback or you are
|
|
352
|
+
using the "client credentials" flow, then the client will login
|
|
353
|
+
automatically when it needs to.
|
|
354
|
+
|
|
355
|
+
However, if you are using the "resource owner credentials" flow
|
|
356
|
+
and you have not provided a login prompt callback (or have not
|
|
357
|
+
previously logged in and stored a token which is still active),
|
|
358
|
+
then you will need to call this method manually to login.
|
|
359
|
+
|
|
360
|
+
Arguments:
|
|
361
|
+
---------
|
|
362
|
+
username: str | None
|
|
363
|
+
The username to use when logging in.
|
|
364
|
+
|
|
365
|
+
password: str | None
|
|
366
|
+
The password to use when logging in.
|
|
367
|
+
|
|
368
|
+
"""
|
|
369
|
+
if self._flow == CLIENT_CREDENTIALS_FLOW:
|
|
370
|
+
self._login_client_credentials()
|
|
371
|
+
elif self._flow == RESOURCE_OWNER_CREDENTIALS_FLOW:
|
|
372
|
+
if self._login_prompt:
|
|
373
|
+
while True:
|
|
374
|
+
try:
|
|
375
|
+
if self._load_token() is not None:
|
|
376
|
+
self._login_resource_owner_credentials(None, None)
|
|
377
|
+
else:
|
|
378
|
+
username, password = self._login_prompt()
|
|
379
|
+
self._login_resource_owner_credentials(
|
|
380
|
+
username, password
|
|
381
|
+
)
|
|
382
|
+
break
|
|
383
|
+
except InvalidUserCredentialsError:
|
|
384
|
+
self._save_token(None)
|
|
385
|
+
else:
|
|
386
|
+
self._login_resource_owner_credentials(username, password)
|
|
387
|
+
else:
|
|
388
|
+
raise AssertionError
|
|
389
|
+
|
|
390
|
+
def _login_client_credentials(self):
|
|
391
|
+
with warnings.catch_warnings():
|
|
392
|
+
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
|
393
|
+
self._oauth_client = requests_oauthlib.OAuth2Session(
|
|
394
|
+
client=oauthlib.oauth2.BackendApplicationClient(
|
|
395
|
+
client_id=self._client_id,
|
|
396
|
+
client_secret=self._client_secret,
|
|
397
|
+
scope=self._scopes,
|
|
398
|
+
),
|
|
399
|
+
scope=self._scopes,
|
|
400
|
+
)
|
|
401
|
+
try:
|
|
402
|
+
token = self._oauth_client.fetch_token(
|
|
403
|
+
self._url + "/oauth/token",
|
|
404
|
+
client_id=self._client_id,
|
|
405
|
+
client_secret=self._client_secret,
|
|
406
|
+
include_client_id=True,
|
|
407
|
+
verify=self._should_verify(),
|
|
408
|
+
scope=self._scopes,
|
|
409
|
+
timeout=self._timeout,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
except oauthlib.oauth2.rfc6749.errors.InvalidClientError as error:
|
|
413
|
+
msg = (
|
|
414
|
+
"You seem to have provided some invalid client "
|
|
415
|
+
"credentials. Are you sure they are correct?"
|
|
416
|
+
)
|
|
417
|
+
raise InvalidClientCredentialsError(msg) from error
|
|
418
|
+
|
|
419
|
+
self._save_token(token)
|
|
420
|
+
|
|
421
|
+
def _login_resource_owner_credentials(self, username, password):
|
|
422
|
+
if not username or not password:
|
|
423
|
+
token = self._load_token()
|
|
424
|
+
if not token:
|
|
425
|
+
msg = (
|
|
426
|
+
"A username and/or password was not provided when logging "
|
|
427
|
+
"in."
|
|
428
|
+
)
|
|
429
|
+
raise LoginRequiredError(msg)
|
|
430
|
+
else:
|
|
431
|
+
initial_oauth_client = requests_oauthlib.OAuth2Session(
|
|
432
|
+
client=oauthlib.oauth2.LegacyApplicationClient(
|
|
433
|
+
client_id=self._client_id,
|
|
434
|
+
client_secret=self._client_secret,
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
token = initial_oauth_client.fetch_token(
|
|
440
|
+
self._url + "/oauth/token",
|
|
441
|
+
username=username,
|
|
442
|
+
password=password,
|
|
443
|
+
client_id=self._client_id,
|
|
444
|
+
client_secret=self._client_secret,
|
|
445
|
+
include_client_id=True,
|
|
446
|
+
verify=self._should_verify(),
|
|
447
|
+
scope=self._scopes,
|
|
448
|
+
timeout=self._timeout,
|
|
449
|
+
)
|
|
450
|
+
self._save_token(token)
|
|
451
|
+
except oauthlib.oauth2.rfc6749.errors.InvalidClientError as error:
|
|
452
|
+
msg = (
|
|
453
|
+
"You seem to have provided some invalid client "
|
|
454
|
+
"credentials. Are you sure they are correct?"
|
|
455
|
+
)
|
|
456
|
+
raise InvalidClientCredentialsError(msg) from error
|
|
457
|
+
except oauthlib.oauth2.rfc6749.errors.InvalidGrantError as error:
|
|
458
|
+
if "(invalid_grant)" in str(
|
|
459
|
+
error
|
|
460
|
+
) and "username & password" in str(error):
|
|
461
|
+
msg = (
|
|
462
|
+
"The user seems to have provided an invalid "
|
|
463
|
+
"username and/or password. They should be shown "
|
|
464
|
+
"an appropriate error message and prompted to try "
|
|
465
|
+
"again."
|
|
466
|
+
)
|
|
467
|
+
raise InvalidUserCredentialsError(msg) from error
|
|
468
|
+
|
|
469
|
+
raise
|
|
470
|
+
auto_refresh_kwargs = {
|
|
471
|
+
"client_id": self._client_id,
|
|
472
|
+
"verify": self._should_verify(),
|
|
473
|
+
}
|
|
474
|
+
if self._client_secret:
|
|
475
|
+
auto_refresh_kwargs["client_secret"] = self._client_secret
|
|
476
|
+
|
|
477
|
+
self._oauth_client = requests_oauthlib.OAuth2Session(
|
|
478
|
+
self._client_id,
|
|
479
|
+
token=token,
|
|
480
|
+
scope=self._scopes,
|
|
481
|
+
auto_refresh_url=self._url + "/oauth/token",
|
|
482
|
+
auto_refresh_kwargs=auto_refresh_kwargs,
|
|
483
|
+
token_updater=self._save_token,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class ONCatObject:
|
|
488
|
+
"""A representation of a JSON object returned from the ONCat API.
|
|
489
|
+
|
|
490
|
+
Note that in practice, this class should probably not be
|
|
491
|
+
instantiated directly, but rather should only be returned by the
|
|
492
|
+
ONCat client after a request is made to the API.
|
|
493
|
+
|
|
494
|
+
This class is a convenience wrapper around a dictionary that
|
|
495
|
+
represents a JSON object returned from the ONCat API. All objects
|
|
496
|
+
from the API are wrapped in this class before being returned.
|
|
497
|
+
|
|
498
|
+
The class provides a way to access top-level fields as if they were
|
|
499
|
+
attributes of the underlying dictionary, ways to access nested
|
|
500
|
+
fields using dot-delimited string notation, as well as a way to list
|
|
501
|
+
all the "nodes" in the (possibly nested) dictionary.
|
|
502
|
+
|
|
503
|
+
For a complete description of this class, including example usage,
|
|
504
|
+
please see the documentation on the ONCat website.
|
|
505
|
+
|
|
506
|
+
Arguments:
|
|
507
|
+
---------
|
|
508
|
+
content: A dictionary
|
|
509
|
+
|
|
510
|
+
Attributes:
|
|
511
|
+
----------
|
|
512
|
+
object: str
|
|
513
|
+
The type of object that this represents (e.g. "facility",
|
|
514
|
+
"experiment", etc.).
|
|
515
|
+
|
|
516
|
+
id: Any
|
|
517
|
+
The unique identifier for this object.
|
|
518
|
+
|
|
519
|
+
"""
|
|
520
|
+
|
|
521
|
+
def __init__(self, content: dict):
|
|
522
|
+
self._content = content
|
|
523
|
+
|
|
524
|
+
def __repr__(self):
|
|
525
|
+
# https://stackoverflow.com/a/2626364/778572
|
|
526
|
+
ident_parts = [
|
|
527
|
+
type(self).__name__,
|
|
528
|
+
"object={}".format(self._content.get("object")),
|
|
529
|
+
"id={}".format(self._content.get("id")),
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
unicode_repr = "<{} at {}> JSON: {}".format(
|
|
533
|
+
" ".join(ident_parts),
|
|
534
|
+
hex(id(self)),
|
|
535
|
+
str(self),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
if sys.version_info[0] < 3:
|
|
539
|
+
return unicode_repr.encode("utf-8")
|
|
540
|
+
return unicode_repr
|
|
541
|
+
|
|
542
|
+
def __str__(self):
|
|
543
|
+
return json.dumps(self._content, sort_keys=True, indent=4)
|
|
544
|
+
|
|
545
|
+
def __getattr__(self, k):
|
|
546
|
+
if k[0] == "_":
|
|
547
|
+
raise AttributeError(k)
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
return self._content[k]
|
|
551
|
+
except KeyError as e:
|
|
552
|
+
raise AttributeError(*e.args) from e
|
|
553
|
+
|
|
554
|
+
def __getitem__(self, key):
|
|
555
|
+
elements = key.split(".")
|
|
556
|
+
current = elements[0]
|
|
557
|
+
remainder = elements[1:] if len(elements) > 1 else None
|
|
558
|
+
if isinstance(self._content[current], dict):
|
|
559
|
+
value = ONCatObject(content=self._content[current])
|
|
560
|
+
|
|
561
|
+
if remainder:
|
|
562
|
+
remainder = ".".join(remainder)
|
|
563
|
+
return value.__getitem__(remainder)
|
|
564
|
+
elif remainder:
|
|
565
|
+
raise KeyError(".".join(remainder))
|
|
566
|
+
else:
|
|
567
|
+
value = self._content[current]
|
|
568
|
+
|
|
569
|
+
return value
|
|
570
|
+
|
|
571
|
+
def to_dict(self) -> dict:
|
|
572
|
+
"""Return the underlying dictionary representation of the
|
|
573
|
+
ONCatObject."""
|
|
574
|
+
return self._content
|
|
575
|
+
|
|
576
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
577
|
+
"""Get a value from the ONCatObject.
|
|
578
|
+
|
|
579
|
+
This method is similar to the `get` method of a dictionary, but
|
|
580
|
+
it allows for dot-delimited string notation to access nested
|
|
581
|
+
fields, e.g., `item.get("nested.field")` is roughly equivalent
|
|
582
|
+
to `item.get("nested").get("field")` or
|
|
583
|
+
`item["nested"]["field"]`.
|
|
584
|
+
|
|
585
|
+
Arguments:
|
|
586
|
+
---------
|
|
587
|
+
key: str
|
|
588
|
+
The key to access in the ONCatObject.
|
|
589
|
+
|
|
590
|
+
default: Any
|
|
591
|
+
The value to return if the key is not found.
|
|
592
|
+
|
|
593
|
+
Returns:
|
|
594
|
+
-------
|
|
595
|
+
The value of the key in the ONCatObject, or the default value
|
|
596
|
+
if the key is not found.
|
|
597
|
+
|
|
598
|
+
"""
|
|
599
|
+
try:
|
|
600
|
+
return self[key]
|
|
601
|
+
except KeyError:
|
|
602
|
+
return default
|
|
603
|
+
|
|
604
|
+
def nodes(
|
|
605
|
+
self, root: str | None = None, include_branches: bool = False
|
|
606
|
+
) -> list[str]:
|
|
607
|
+
"""List all the "nodes" in the ONCatObject.
|
|
608
|
+
|
|
609
|
+
This method returns a list of all the full paths that exist in
|
|
610
|
+
the ONCatObject, which can be used to access the values that are
|
|
611
|
+
contained within it.
|
|
612
|
+
|
|
613
|
+
If a `root` is provided, only the keys that start with that
|
|
614
|
+
string will be returned.
|
|
615
|
+
|
|
616
|
+
If `include_branches` is set to `True`, then the keys that are
|
|
617
|
+
dictionaries will also be returned, else only the keys that are
|
|
618
|
+
not dictionaries (i.e., "leaves") will be returned.
|
|
619
|
+
|
|
620
|
+
Arguments:
|
|
621
|
+
---------
|
|
622
|
+
root: str
|
|
623
|
+
The dot-delimited root path to filter the nodes by.
|
|
624
|
+
|
|
625
|
+
include_branches: bool
|
|
626
|
+
Whether or not to include the branch nodes.
|
|
627
|
+
|
|
628
|
+
"""
|
|
629
|
+
result = sorted(_yield_nodes(self._content, None, include_branches))
|
|
630
|
+
|
|
631
|
+
if root:
|
|
632
|
+
# TODO: Not the most performant way of doing this...
|
|
633
|
+
return [key for key in result if key.startswith(root)]
|
|
634
|
+
return result
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# Alias name for backwards compatibility.
|
|
638
|
+
ONCatRepresentation = ONCatObject
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
########################################################################
|
|
642
|
+
# Mixins
|
|
643
|
+
########################################################################
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class _ONCatMixinMeta(type):
|
|
647
|
+
REGISTERED_RESOURCES: ClassVar[list] = []
|
|
648
|
+
|
|
649
|
+
def __init__(cls, *args):
|
|
650
|
+
super().__init__(args)
|
|
651
|
+
if not cls.__name__.startswith("_"):
|
|
652
|
+
_ONCatMixinMeta.REGISTERED_RESOURCES.append(cls)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class _ONCatResource(metaclass=_ONCatMixinMeta):
|
|
656
|
+
def __init__(self, parent_oncat):
|
|
657
|
+
self._parent_oncat_weakref = weakref.ref(parent_oncat)
|
|
658
|
+
|
|
659
|
+
def retrieve(self, identifier, **kwargs):
|
|
660
|
+
oncat = self._parent_oncat_weakref()
|
|
661
|
+
|
|
662
|
+
if identifier.startswith("/"):
|
|
663
|
+
identifier = identifier[1:]
|
|
664
|
+
|
|
665
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}/{identifier}"
|
|
666
|
+
|
|
667
|
+
return ONCatObject(content=oncat._get(path, **kwargs))
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _ListableONCatMixin(_ONCatResource):
|
|
671
|
+
def list(self, **kwargs):
|
|
672
|
+
oncat = self._parent_oncat_weakref()
|
|
673
|
+
|
|
674
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}"
|
|
675
|
+
|
|
676
|
+
return [
|
|
677
|
+
ONCatObject(content=content)
|
|
678
|
+
for content in oncat._get(path, **kwargs)
|
|
679
|
+
]
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class _UpdatableONCatMixin(_ONCatResource):
|
|
683
|
+
def place(self, identifier, data, **kwargs):
|
|
684
|
+
oncat = self._parent_oncat_weakref()
|
|
685
|
+
|
|
686
|
+
if identifier.startswith("/"):
|
|
687
|
+
identifier = identifier[1:]
|
|
688
|
+
|
|
689
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}/{identifier}"
|
|
690
|
+
|
|
691
|
+
return ONCatObject(content=oncat._put(path, data, **kwargs))
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class _CreatableONCatMixin(_ONCatResource):
|
|
695
|
+
def create(self, data, **kwargs):
|
|
696
|
+
oncat = self._parent_oncat_weakref()
|
|
697
|
+
|
|
698
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}"
|
|
699
|
+
|
|
700
|
+
return ONCatObject(content=oncat._post(path, data, **kwargs))
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class _RemovableONCatMixin(_ONCatResource):
|
|
704
|
+
def remove(self, identifier, **kwargs):
|
|
705
|
+
oncat = self._parent_oncat_weakref()
|
|
706
|
+
|
|
707
|
+
if identifier.startswith("/"):
|
|
708
|
+
identifier = identifier[1:]
|
|
709
|
+
|
|
710
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}/{identifier}"
|
|
711
|
+
|
|
712
|
+
oncat._delete(path, **kwargs)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class _ControllerONCatMixin(_ONCatResource):
|
|
716
|
+
def __getattr__(self, k):
|
|
717
|
+
if k[0] == "_":
|
|
718
|
+
raise AttributeError(k)
|
|
719
|
+
|
|
720
|
+
return functools.partial(self._controller, k)
|
|
721
|
+
|
|
722
|
+
def _controller(self, name, *args, **kwargs):
|
|
723
|
+
if len(args) == 0 or len(args) > 2:
|
|
724
|
+
raise TypeError
|
|
725
|
+
if len(args) == 1:
|
|
726
|
+
if isinstance(args[0], str):
|
|
727
|
+
identifier = args[0]
|
|
728
|
+
data = None
|
|
729
|
+
else:
|
|
730
|
+
identifier = None
|
|
731
|
+
data = args[0]
|
|
732
|
+
else:
|
|
733
|
+
if not isinstance(args[0], str):
|
|
734
|
+
raise TypeError
|
|
735
|
+
identifier = args[0]
|
|
736
|
+
data = args[1]
|
|
737
|
+
|
|
738
|
+
if identifier is not None and identifier.startswith("/"):
|
|
739
|
+
identifier = identifier[1:]
|
|
740
|
+
|
|
741
|
+
oncat = self._parent_oncat_weakref()
|
|
742
|
+
|
|
743
|
+
if identifier is not None:
|
|
744
|
+
path = (
|
|
745
|
+
f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}/"
|
|
746
|
+
+ f"{identifier}/{name}"
|
|
747
|
+
)
|
|
748
|
+
else:
|
|
749
|
+
path = f"{self._NAMESPACE}/{self._RESOURCE_ENDPOINT}/{name}"
|
|
750
|
+
|
|
751
|
+
return ONCatObject(content=oncat._post(path, data, **kwargs))
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
########################################################################
|
|
755
|
+
# Resources
|
|
756
|
+
########################################################################
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
class Facility(_ListableONCatMixin):
|
|
760
|
+
_RESOURCE_ENDPOINT = "facilities"
|
|
761
|
+
_NAMESPACE = "api"
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class Instrument(_ListableONCatMixin):
|
|
765
|
+
_RESOURCE_ENDPOINT = "instruments"
|
|
766
|
+
_NAMESPACE = "api"
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
class Experiment(_ListableONCatMixin):
|
|
770
|
+
_RESOURCE_ENDPOINT = "experiments"
|
|
771
|
+
_NAMESPACE = "api"
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
class Run(_ListableONCatMixin):
|
|
775
|
+
_RESOURCE_ENDPOINT = "runs"
|
|
776
|
+
_NAMESPACE = "experimental"
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
class Group(_ListableONCatMixin):
|
|
780
|
+
_RESOURCE_ENDPOINT = "groups"
|
|
781
|
+
_NAMESPACE = "experimental"
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
class Grouping(_ListableONCatMixin):
|
|
785
|
+
_RESOURCE_ENDPOINT = "groupings"
|
|
786
|
+
_NAMESPACE = "experimental"
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
class Mapping(_ListableONCatMixin):
|
|
790
|
+
_RESOURCE_ENDPOINT = "mappings"
|
|
791
|
+
_NAMESPACE = "experimental"
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
class Proposal(_ListableONCatMixin):
|
|
795
|
+
_RESOURCE_ENDPOINT = "proposals"
|
|
796
|
+
_NAMESPACE = "experimental"
|
|
797
|
+
|
|
798
|
+
|
|
799
|
+
class Item(_ListableONCatMixin):
|
|
800
|
+
_RESOURCE_ENDPOINT = "items"
|
|
801
|
+
_NAMESPACE = "experimental"
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
class Archive(
|
|
805
|
+
_ListableONCatMixin, _RemovableONCatMixin, _ControllerONCatMixin
|
|
806
|
+
):
|
|
807
|
+
_RESOURCE_ENDPOINT = "archives"
|
|
808
|
+
_NAMESPACE = "api"
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
class Datafile(
|
|
812
|
+
_ControllerONCatMixin,
|
|
813
|
+
_CreatableONCatMixin,
|
|
814
|
+
_ListableONCatMixin,
|
|
815
|
+
_RemovableONCatMixin,
|
|
816
|
+
):
|
|
817
|
+
_RESOURCE_ENDPOINT = "datafiles"
|
|
818
|
+
_NAMESPACE = "api"
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
class Reduction(
|
|
822
|
+
_ControllerONCatMixin,
|
|
823
|
+
_CreatableONCatMixin,
|
|
824
|
+
_ListableONCatMixin,
|
|
825
|
+
_RemovableONCatMixin,
|
|
826
|
+
):
|
|
827
|
+
_RESOURCE_ENDPOINT = "reductions"
|
|
828
|
+
_NAMESPACE = "api"
|
|
829
|
+
|
|
830
|
+
|
|
831
|
+
class Simulation(_ControllerONCatMixin, _ListableONCatMixin):
|
|
832
|
+
_RESOURCE_ENDPOINT = "simulations"
|
|
833
|
+
_NAMESPACE = "api"
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class User(_ListableONCatMixin):
|
|
837
|
+
_RESOURCE_ENDPOINT = "users"
|
|
838
|
+
_NAMESPACE = "api"
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class Task(_ListableONCatMixin):
|
|
842
|
+
_RESOURCE_ENDPOINT = "tasks"
|
|
843
|
+
_NAMESPACE = "api"
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
########################################################################
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
class Info(_ONCatResource):
|
|
850
|
+
_RESOURCE_ENDPOINT = "info"
|
|
851
|
+
_NAMESPACE = "data"
|
|
852
|
+
|
|
853
|
+
|
|
854
|
+
########################################################################
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class CacheEntry(_UpdatableONCatMixin):
|
|
858
|
+
_RESOURCE_ENDPOINT = "cache"
|
|
859
|
+
_NAMESPACE = "admin"
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
class UsageEntry(_ListableONCatMixin):
|
|
863
|
+
_RESOURCE_ENDPOINT = "usage"
|
|
864
|
+
_NAMESPACE = "admin"
|
|
865
|
+
|
|
866
|
+
|
|
867
|
+
########################################################################
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
class Template(
|
|
871
|
+
_CreatableONCatMixin, _ListableONCatMixin, _RemovableONCatMixin
|
|
872
|
+
):
|
|
873
|
+
_RESOURCE_ENDPOINT = "templates"
|
|
874
|
+
_NAMESPACE = "settings"
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
########################################################################
|
|
878
|
+
# Misc Tools
|
|
879
|
+
########################################################################
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
class _InMemoryTokenStore:
|
|
883
|
+
"""A token store that persists OAuth tokens in memory only.
|
|
884
|
+
|
|
885
|
+
This kind of store is only really useful for testing or as an
|
|
886
|
+
example of the interface a token store should provide; in its
|
|
887
|
+
current form it does not offer anything that the `ONCat` client
|
|
888
|
+
itself does not already provide when leaving the getter and setter
|
|
889
|
+
parameters empty.
|
|
890
|
+
"""
|
|
891
|
+
|
|
892
|
+
def __init__(self):
|
|
893
|
+
self._token = None
|
|
894
|
+
|
|
895
|
+
def set_token(self, token: str | None):
|
|
896
|
+
self._token = token
|
|
897
|
+
|
|
898
|
+
def get_token(self) -> str | None:
|
|
899
|
+
return self._token
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
class FileSystemTokenStore:
|
|
903
|
+
"""A token store that persists OAuth tokens to a file on disk.
|
|
904
|
+
|
|
905
|
+
The file is created if it doesn't already exist, however the parent
|
|
906
|
+
directory must exist and you must have write permissions else an
|
|
907
|
+
exception will be raised.
|
|
908
|
+
|
|
909
|
+
This kind of store may be useful for long-lived applications where
|
|
910
|
+
you need to persist the token across uses of the client, and it is
|
|
911
|
+
an instance of an application that is run by a single user. This
|
|
912
|
+
kind of store is not suitable for applications that have the
|
|
913
|
+
expectation of being able to manage sessions for multiple users.
|
|
914
|
+
|
|
915
|
+
Arguments:
|
|
916
|
+
---------
|
|
917
|
+
path: str
|
|
918
|
+
The path to the file where the token will be stored.
|
|
919
|
+
|
|
920
|
+
"""
|
|
921
|
+
|
|
922
|
+
def __init__(self, path: str):
|
|
923
|
+
self.path = path
|
|
924
|
+
|
|
925
|
+
def read_token(self) -> str | None:
|
|
926
|
+
"""Read the token from the file on disk, if it exists.
|
|
927
|
+
|
|
928
|
+
Return:
|
|
929
|
+
------
|
|
930
|
+
The token if it exists, else `None`.
|
|
931
|
+
|
|
932
|
+
"""
|
|
933
|
+
if not os.path.exists(self.path):
|
|
934
|
+
return None
|
|
935
|
+
|
|
936
|
+
with open(self.path) as storage:
|
|
937
|
+
return json.load(storage)
|
|
938
|
+
|
|
939
|
+
def write_token(self, token: str | None):
|
|
940
|
+
"""Write the token to the file on disk.
|
|
941
|
+
|
|
942
|
+
Arguments:
|
|
943
|
+
---------
|
|
944
|
+
token: str | None
|
|
945
|
+
The token to write to the file, or `None` to clear the
|
|
946
|
+
token.
|
|
947
|
+
|
|
948
|
+
"""
|
|
949
|
+
with open(self.path, "w") as storage:
|
|
950
|
+
json.dump(token, storage)
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def prompt_user_for_login() -> tuple[str, str]:
|
|
954
|
+
"""Prompt the user for their login credentials via the command line."""
|
|
955
|
+
print("Enter UCAMS / XCAMS...")
|
|
956
|
+
username = input("Username: ")
|
|
957
|
+
password = getpass.getpass("Password: ")
|
|
958
|
+
|
|
959
|
+
return username, password
|
|
960
|
+
|
|
961
|
+
|
|
962
|
+
class _UserConfigFile:
|
|
963
|
+
"""Subject to change and NOT recommended for use by end-users."""
|
|
964
|
+
|
|
965
|
+
def __init__(self, client_name, path=None):
|
|
966
|
+
self._client_name = client_name
|
|
967
|
+
self.path = (
|
|
968
|
+
os.path.join(os.path.expanduser("~"), ".oncatrc")
|
|
969
|
+
if path is None
|
|
970
|
+
else path
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def _content(self):
|
|
974
|
+
with open(self.path) as user_config_file:
|
|
975
|
+
return json.load(user_config_file)
|
|
976
|
+
|
|
977
|
+
def client_id(self):
|
|
978
|
+
return self._content()[self._client_name]["client_id"]
|
|
979
|
+
|
|
980
|
+
def client_secret(self):
|
|
981
|
+
return self._content()[self._client_name]["client_secret"]
|
|
982
|
+
|
|
983
|
+
def token_getter(self):
|
|
984
|
+
return self._content()[self._client_name].get("current_token", None)
|
|
985
|
+
|
|
986
|
+
def token_setter(self, token):
|
|
987
|
+
content = self._content()
|
|
988
|
+
content[self._client_name]["current_token"] = token
|
|
989
|
+
|
|
990
|
+
pathlib.Path(self.path).write_text(
|
|
991
|
+
json.dumps(content, indent=4, sort_keys=True)
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
|
|
995
|
+
########################################################################
|
|
996
|
+
# Helpers
|
|
997
|
+
########################################################################
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
def _yield_nodes(d, path, include_branches):
|
|
1001
|
+
for node in d:
|
|
1002
|
+
node_path = f"{path}.{node}" if path else node
|
|
1003
|
+
if isinstance(d[node], dict):
|
|
1004
|
+
if include_branches:
|
|
1005
|
+
yield node_path
|
|
1006
|
+
yield from _yield_nodes(d[node], node_path, include_branches)
|
|
1007
|
+
else:
|
|
1008
|
+
yield node_path
|
|
1009
|
+
|
|
1010
|
+
|
|
1011
|
+
########################################################################
|
|
1012
|
+
# Exceptions
|
|
1013
|
+
########################################################################
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class PyONCatError(Exception):
|
|
1017
|
+
"""A base exception for all errors that may be raised by PyONCat."""
|
|
1018
|
+
|
|
1019
|
+
def __init__(self, message=None, original_error=None):
|
|
1020
|
+
if not message:
|
|
1021
|
+
message = ""
|
|
1022
|
+
with contextlib.suppress(Exception):
|
|
1023
|
+
message += " [" + original_error.response.json()["message"] + "]"
|
|
1024
|
+
super().__init__(message)
|
|
1025
|
+
self.original_error = original_error
|
|
1026
|
+
|
|
1027
|
+
|
|
1028
|
+
class UnauthorizedError(PyONCatError):
|
|
1029
|
+
"""An error that is raised when the client is not authorized to
|
|
1030
|
+
access a resource.
|
|
1031
|
+
"""
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
class InvalidClientCredentialsError(PyONCatError):
|
|
1035
|
+
"""An error that is raised when the client credentials provided are
|
|
1036
|
+
invalid.
|
|
1037
|
+
"""
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
class InvalidUserCredentialsError(PyONCatError):
|
|
1041
|
+
"""An error that is raised when the user credentials provided are
|
|
1042
|
+
invalid.
|
|
1043
|
+
"""
|
|
1044
|
+
|
|
1045
|
+
|
|
1046
|
+
class InvalidRefreshTokenError(PyONCatError):
|
|
1047
|
+
"""An error that is raised when the refresh token provided is
|
|
1048
|
+
invalid.
|
|
1049
|
+
"""
|
|
1050
|
+
|
|
1051
|
+
|
|
1052
|
+
class LoginRequiredError(PyONCatError):
|
|
1053
|
+
"""An error that is raised when a login is required to access a
|
|
1054
|
+
resource.
|
|
1055
|
+
"""
|
|
1056
|
+
|
|
1057
|
+
|
|
1058
|
+
class NotFoundError(PyONCatError):
|
|
1059
|
+
"""An error that is raised when a resource cannot be found."""
|
|
1060
|
+
|
|
1061
|
+
|
|
1062
|
+
class BadRequestError(PyONCatError):
|
|
1063
|
+
"""An error that is raised when a bad request is made to the API.
|
|
1064
|
+
|
|
1065
|
+
Check your input and try again.
|
|
1066
|
+
"""
|