elapi 1.0.7.dev3__py3-none-any.whl → 1.0.7.dev5__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.
- elapi/VERSION +1 -1
- elapi/_names.py +3 -0
- elapi/api/__init__.py +15 -0
- elapi/{api.py → api/api.py} +130 -53
- elapi/{validators/permission.py → api/validators.py} +103 -6
- elapi/cli/__init__.py +0 -1
- elapi/cli/__main__.py +2 -1
- elapi/cli/_plugin_handler.py +4 -1
- elapi/cli/doc.py +19 -1
- elapi/cli/elapi.py +409 -103
- elapi/configuration/__init__.py +20 -0
- elapi/configuration/_config_history.py +49 -3
- elapi/configuration/config.py +63 -81
- elapi/configuration/log_file.py +1 -1
- elapi/configuration/overridable_vars.py +45 -0
- elapi/configuration/validators.py +556 -0
- elapi/{validators → core_validators}/__init__.py +0 -2
- elapi/plugins/bill_teams/bill_teams.py +4 -4
- elapi/plugins/bill_teams/cli.py +472 -349
- elapi/plugins/bill_teams/generate_table.py +13 -0
- elapi/plugins/bill_teams/specification.py +47 -0
- elapi/plugins/bill_teams/{validator.py → validators.py} +168 -32
- elapi/plugins/commons/__init__.py +1 -0
- elapi/plugins/commons/cli_helpers.py +36 -3
- elapi/plugins/commons/export.py +5 -4
- elapi/plugins/commons/get_information.py +2 -2
- elapi/plugins/commons/get_location_from_headers.py +3 -1
- elapi/plugins/experiments/cli.py +19 -21
- elapi/plugins/experiments/experiments.py +2 -2
- elapi/plugins/show_config.py +87 -13
- elapi/validators.py +24 -0
- {elapi-1.0.7.dev3.dist-info → elapi-1.0.7.dev5.dist-info}/METADATA +35 -5
- elapi-1.0.7.dev5.dist-info/RECORD +63 -0
- elapi/validators/identity.py +0 -163
- elapi-1.0.7.dev3.dist-info/RECORD +0 -58
- /elapi/{endpoint.py → api/endpoint.py} +0 -0
- /elapi/{validators → core_validators}/base.py +0 -0
- /elapi/{validators → core_validators}/path.py +0 -0
- {elapi-1.0.7.dev3.dist-info → elapi-1.0.7.dev5.dist-info}/LICENSE +0 -0
- {elapi-1.0.7.dev3.dist-info → elapi-1.0.7.dev5.dist-info}/WHEEL +0 -0
- {elapi-1.0.7.dev3.dist-info → elapi-1.0.7.dev5.dist-info}/entry_points.txt +0 -0
elapi/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.7.
|
|
1
|
+
1.0.7.dev5
|
elapi/_names.py
CHANGED
|
@@ -38,6 +38,9 @@ KEY_HOST: str = "HOST"
|
|
|
38
38
|
KEY_API_TOKEN: str = "API_TOKEN"
|
|
39
39
|
KEY_EXPORT_DIR: str = "EXPORT_DIR"
|
|
40
40
|
KEY_UNSAFE_TOKEN_WARNING: str = "UNSAFE_API_TOKEN_WARNING"
|
|
41
|
+
KEY_ENABLE_HTTP2: str = "ENABLE_HTTP2"
|
|
42
|
+
KEY_VERIFY_SSL: str = "VERIFY_SSL"
|
|
43
|
+
KEY_TIMEOUT: str = "TIMEOUT"
|
|
41
44
|
|
|
42
45
|
# Log data directory with root permission
|
|
43
46
|
LOG_DIR_ROOT: Path = Path(f"/var/log/{APP_NAME}")
|
elapi/api/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# ruff: noqa: F401
|
|
2
|
+
from .api import (
|
|
3
|
+
APIRequest,
|
|
4
|
+
ElabFTWURLError,
|
|
5
|
+
ElabFTWURL,
|
|
6
|
+
GETRequest,
|
|
7
|
+
AsyncGETRequest,
|
|
8
|
+
POSTRequest,
|
|
9
|
+
AsyncPOSTRequest,
|
|
10
|
+
PATCHRequest,
|
|
11
|
+
AsyncPATCHRequest,
|
|
12
|
+
DELETERequest,
|
|
13
|
+
AsyncDELETERequest,
|
|
14
|
+
)
|
|
15
|
+
from .endpoint import FixedEndpoint, FixedAsyncEndpoint
|
elapi/{api.py → api/api.py}
RENAMED
|
@@ -5,21 +5,33 @@ from typing import Union, Optional, Tuple
|
|
|
5
5
|
from httpx import Response, Client, AsyncClient, Limits
|
|
6
6
|
from httpx_auth import HeaderApiKey
|
|
7
7
|
|
|
8
|
-
from
|
|
8
|
+
from ..configuration import (
|
|
9
|
+
TOKEN_BEARER,
|
|
10
|
+
get_active_host,
|
|
11
|
+
get_active_api_token,
|
|
12
|
+
get_active_enable_http2,
|
|
13
|
+
get_active_verify_ssl,
|
|
14
|
+
get_active_timeout,
|
|
15
|
+
)
|
|
9
16
|
|
|
10
17
|
|
|
11
18
|
class APIRequest(ABC):
|
|
12
|
-
__slots__ = "keep_session_open", "_client"
|
|
13
|
-
host: str = HOST
|
|
14
|
-
api_token: str = API_TOKEN
|
|
15
|
-
header_name: str = TOKEN_BEARER
|
|
19
|
+
__slots__ = "keep_session_open", "_client", "host", "api_token", "header_name"
|
|
16
20
|
|
|
17
21
|
def __init__(self, keep_session_open: bool = False, **kwargs):
|
|
22
|
+
self.host: str = get_active_host()
|
|
23
|
+
self.api_token: str = get_active_api_token().token
|
|
24
|
+
self.header_name: str = TOKEN_BEARER
|
|
18
25
|
self.keep_session_open = keep_session_open
|
|
26
|
+
enable_http2 = get_active_enable_http2()
|
|
27
|
+
verify_ssl = get_active_verify_ssl()
|
|
28
|
+
timeout = get_active_timeout()
|
|
19
29
|
_client = Client if not self.is_async_client else AsyncClient
|
|
20
30
|
self._client: Union[Client, AsyncClient] = _client(
|
|
21
31
|
auth=HeaderApiKey(api_key=self.api_token, header_name=self.header_name),
|
|
22
|
-
|
|
32
|
+
http2=enable_http2,
|
|
33
|
+
verify=verify_ssl,
|
|
34
|
+
timeout=timeout,
|
|
23
35
|
**kwargs,
|
|
24
36
|
)
|
|
25
37
|
|
|
@@ -94,13 +106,15 @@ class ElabFTWURL:
|
|
|
94
106
|
"experiments_status",
|
|
95
107
|
"items_status",
|
|
96
108
|
"teamgroups",
|
|
109
|
+
"tags",
|
|
97
110
|
),
|
|
98
111
|
"todolist": (),
|
|
99
112
|
"unfinished_steps": (),
|
|
100
113
|
"users": ("notifications",),
|
|
101
114
|
"idps": (),
|
|
115
|
+
"import": (),
|
|
116
|
+
"exports": (),
|
|
102
117
|
}
|
|
103
|
-
host = APIRequest.host
|
|
104
118
|
|
|
105
119
|
def __init__(
|
|
106
120
|
self,
|
|
@@ -116,6 +130,18 @@ class ElabFTWURL:
|
|
|
116
130
|
self.sub_endpoint_id = sub_endpoint_id
|
|
117
131
|
self.query = query
|
|
118
132
|
|
|
133
|
+
@property
|
|
134
|
+
def host(self) -> str:
|
|
135
|
+
return get_active_host()
|
|
136
|
+
|
|
137
|
+
@host.setter
|
|
138
|
+
def host(self, value):
|
|
139
|
+
raise AttributeError("Host value cannot be modified!")
|
|
140
|
+
|
|
141
|
+
@host.deleter
|
|
142
|
+
def host(self):
|
|
143
|
+
raise AttributeError("Host cannot be deleted!")
|
|
144
|
+
|
|
119
145
|
@property
|
|
120
146
|
def endpoint_name(self) -> str:
|
|
121
147
|
return self._endpoint_name
|
|
@@ -138,10 +164,19 @@ class ElabFTWURL:
|
|
|
138
164
|
@sub_endpoint_name.setter
|
|
139
165
|
def sub_endpoint_name(self, value: str):
|
|
140
166
|
if value is not None:
|
|
141
|
-
if value.lower() not in
|
|
167
|
+
if value.lower() not in (
|
|
168
|
+
valid_sub_endpoint_name := ElabFTWURL.VALID_ENDPOINTS[
|
|
169
|
+
self.endpoint_name
|
|
170
|
+
]
|
|
171
|
+
):
|
|
172
|
+
if not valid_sub_endpoint_name:
|
|
173
|
+
raise ElabFTWURLError(
|
|
174
|
+
f"Endpoint '{self._endpoint_name}' does not have any sub-endpoint!"
|
|
175
|
+
)
|
|
176
|
+
|
|
142
177
|
raise ElabFTWURLError(
|
|
143
178
|
f"A Sub-endpoint for endpoint '{self._endpoint_name}' must be "
|
|
144
|
-
f"one of valid eLabFTW sub-endpoints: {', '.join(
|
|
179
|
+
f"one of valid eLabFTW sub-endpoints: {', '.join(valid_sub_endpoint_name)}."
|
|
145
180
|
)
|
|
146
181
|
self._sub_endpoint_name = value
|
|
147
182
|
else:
|
|
@@ -154,7 +189,7 @@ class ElabFTWURL:
|
|
|
154
189
|
@endpoint_id.setter
|
|
155
190
|
def endpoint_id(self, value):
|
|
156
191
|
if value is not None:
|
|
157
|
-
if not re.match(r"^(\d+)$|^(me)$", value := str(value)):
|
|
192
|
+
if not re.match(r"^(\d+)$|^(me)$|^(current)$", value := str(value)):
|
|
158
193
|
# Although, eLabFTW primarily supports integer-only IDs, there are exceptions, like the alias
|
|
159
194
|
# ID "me" for receiving one's own user information.
|
|
160
195
|
raise ElabFTWURLError("Invalid endpoint ID (or entity ID).")
|
|
@@ -173,7 +208,7 @@ class ElabFTWURL:
|
|
|
173
208
|
"Sub-endpoint ID cannot be defined without first specifying its sub-endpoint name."
|
|
174
209
|
)
|
|
175
210
|
if value is not None:
|
|
176
|
-
if not re.match(r"^(\d+)$|^(me)$", value := str(value)):
|
|
211
|
+
if not re.match(r"^(\d+)$|^(me)$|^(current)$", value := str(value)):
|
|
177
212
|
raise ElabFTWURLError("Invalid sub-endpoint ID (or entity sub-ID).")
|
|
178
213
|
self._sub_endpoint_id = value
|
|
179
214
|
else:
|
|
@@ -191,7 +226,7 @@ class ElabFTWURL:
|
|
|
191
226
|
|
|
192
227
|
def get(self) -> str:
|
|
193
228
|
url = (
|
|
194
|
-
f"{
|
|
229
|
+
f"{self.host}/{self.endpoint_name}/{self.endpoint_id}/"
|
|
195
230
|
f"{self.sub_endpoint_name}/{self.sub_endpoint_id}"
|
|
196
231
|
).rstrip("/")
|
|
197
232
|
if self.query:
|
|
@@ -205,12 +240,14 @@ class GETRequest(APIRequest):
|
|
|
205
240
|
def __init__(self, **kwargs):
|
|
206
241
|
super().__init__(**kwargs)
|
|
207
242
|
|
|
208
|
-
def _make(self, *args) -> Response:
|
|
243
|
+
def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
209
244
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
210
245
|
url = ElabFTWURL(
|
|
211
246
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
212
247
|
)
|
|
213
|
-
return super().client.get(
|
|
248
|
+
return super().client.get(
|
|
249
|
+
url.get(), headers=headers or {"Accept": "application/json"}, **kwargs
|
|
250
|
+
)
|
|
214
251
|
|
|
215
252
|
def close(self):
|
|
216
253
|
super().close()
|
|
@@ -222,9 +259,15 @@ class GETRequest(APIRequest):
|
|
|
222
259
|
sub_endpoint_name: Optional[str] = None,
|
|
223
260
|
sub_endpoint_id: Union[int, str, None] = None,
|
|
224
261
|
query: Optional[dict] = None,
|
|
262
|
+
**kwargs,
|
|
225
263
|
) -> Response:
|
|
226
264
|
return super().__call__(
|
|
227
|
-
endpoint_name,
|
|
265
|
+
endpoint_name,
|
|
266
|
+
endpoint_id,
|
|
267
|
+
sub_endpoint_name,
|
|
268
|
+
sub_endpoint_id,
|
|
269
|
+
query,
|
|
270
|
+
**kwargs,
|
|
228
271
|
)
|
|
229
272
|
|
|
230
273
|
|
|
@@ -234,7 +277,7 @@ class POSTRequest(APIRequest):
|
|
|
234
277
|
def __init__(self, **kwargs):
|
|
235
278
|
super().__init__(**kwargs)
|
|
236
279
|
|
|
237
|
-
def _make(self, *args, **kwargs) -> Response:
|
|
280
|
+
def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
238
281
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
239
282
|
url = ElabFTWURL(
|
|
240
283
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
@@ -246,7 +289,8 @@ class POSTRequest(APIRequest):
|
|
|
246
289
|
files = kwargs.pop("files", None)
|
|
247
290
|
return super().client.post(
|
|
248
291
|
url.get(),
|
|
249
|
-
headers=
|
|
292
|
+
headers=headers
|
|
293
|
+
or {
|
|
250
294
|
"Accept": "*/*",
|
|
251
295
|
# If json argument isn't empty, '"Content-Type": "application/json"' is automatically set.
|
|
252
296
|
# '"Content-Type": "multipart/form-data"', takes no effect, and
|
|
@@ -284,16 +328,19 @@ class POSTRequest(APIRequest):
|
|
|
284
328
|
class AsyncPOSTRequest(APIRequest, is_async_client=True):
|
|
285
329
|
__slots__ = ()
|
|
286
330
|
|
|
287
|
-
def __init__(
|
|
331
|
+
def __init__(
|
|
332
|
+
self,
|
|
333
|
+
limits=Limits(
|
|
334
|
+
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
335
|
+
),
|
|
336
|
+
**kwargs,
|
|
337
|
+
):
|
|
288
338
|
super().__init__(
|
|
289
|
-
|
|
290
|
-
limits=Limits(
|
|
291
|
-
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
292
|
-
),
|
|
339
|
+
limits=limits,
|
|
293
340
|
**kwargs,
|
|
294
341
|
)
|
|
295
342
|
|
|
296
|
-
async def _make(self, *args, **kwargs) -> Response:
|
|
343
|
+
async def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
297
344
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
298
345
|
url = ElabFTWURL(
|
|
299
346
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
@@ -305,7 +352,7 @@ class AsyncPOSTRequest(APIRequest, is_async_client=True):
|
|
|
305
352
|
files = kwargs.pop("files", None)
|
|
306
353
|
return await super().client.post(
|
|
307
354
|
url.get(),
|
|
308
|
-
headers={"Accept": "*/*"},
|
|
355
|
+
headers=headers or {"Accept": "*/*"},
|
|
309
356
|
json=data,
|
|
310
357
|
files=files,
|
|
311
358
|
**kwargs,
|
|
@@ -340,23 +387,25 @@ class AsyncPOSTRequest(APIRequest, is_async_client=True):
|
|
|
340
387
|
class AsyncGETRequest(APIRequest, is_async_client=True):
|
|
341
388
|
__slots__ = ()
|
|
342
389
|
|
|
343
|
-
def __init__(
|
|
390
|
+
def __init__(
|
|
391
|
+
self,
|
|
392
|
+
limits=Limits(
|
|
393
|
+
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
394
|
+
),
|
|
395
|
+
**kwargs,
|
|
396
|
+
):
|
|
344
397
|
super().__init__(
|
|
345
|
-
|
|
346
|
-
limits=Limits(
|
|
347
|
-
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
348
|
-
),
|
|
398
|
+
limits=limits,
|
|
349
399
|
**kwargs,
|
|
350
400
|
)
|
|
351
401
|
|
|
352
|
-
async def _make(self, *args) -> Response:
|
|
402
|
+
async def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
353
403
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
354
404
|
url = ElabFTWURL(
|
|
355
405
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
356
406
|
)
|
|
357
407
|
return await super().client.get(
|
|
358
|
-
url.get(),
|
|
359
|
-
headers={"Accept": "application/json"},
|
|
408
|
+
url.get(), headers=headers or {"Accept": "application/json"}, **kwargs
|
|
360
409
|
)
|
|
361
410
|
|
|
362
411
|
async def close(self):
|
|
@@ -370,9 +419,15 @@ class AsyncGETRequest(APIRequest, is_async_client=True):
|
|
|
370
419
|
sub_endpoint_name: Optional[str] = None,
|
|
371
420
|
sub_endpoint_id: Union[int, str, None] = None,
|
|
372
421
|
query: Optional[dict] = None,
|
|
422
|
+
**kwargs,
|
|
373
423
|
) -> Response:
|
|
374
424
|
response = await self._make(
|
|
375
|
-
endpoint_name,
|
|
425
|
+
endpoint_name,
|
|
426
|
+
endpoint_id,
|
|
427
|
+
sub_endpoint_name,
|
|
428
|
+
sub_endpoint_id,
|
|
429
|
+
query,
|
|
430
|
+
**kwargs,
|
|
376
431
|
)
|
|
377
432
|
if not self.keep_session_open:
|
|
378
433
|
await self.close()
|
|
@@ -385,7 +440,7 @@ class PATCHRequest(APIRequest):
|
|
|
385
440
|
def __init__(self, **kwargs):
|
|
386
441
|
super().__init__(**kwargs)
|
|
387
442
|
|
|
388
|
-
def _make(self, *args, **kwargs) -> Response:
|
|
443
|
+
def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
389
444
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
390
445
|
url = ElabFTWURL(
|
|
391
446
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
@@ -396,7 +451,8 @@ class PATCHRequest(APIRequest):
|
|
|
396
451
|
}
|
|
397
452
|
return super().client.patch(
|
|
398
453
|
url.get(),
|
|
399
|
-
headers=
|
|
454
|
+
headers=headers
|
|
455
|
+
or {
|
|
400
456
|
"Accept": "application/json"
|
|
401
457
|
}, # '"Content-Type": "application/json"' is passed automatically when json argument is passed.
|
|
402
458
|
json=data,
|
|
@@ -428,16 +484,19 @@ class PATCHRequest(APIRequest):
|
|
|
428
484
|
class AsyncPATCHRequest(APIRequest, is_async_client=True):
|
|
429
485
|
__slots__ = ()
|
|
430
486
|
|
|
431
|
-
def __init__(
|
|
487
|
+
def __init__(
|
|
488
|
+
self,
|
|
489
|
+
limits=Limits(
|
|
490
|
+
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
491
|
+
),
|
|
492
|
+
**kwargs,
|
|
493
|
+
):
|
|
432
494
|
super().__init__(
|
|
433
|
-
|
|
434
|
-
limits=Limits(
|
|
435
|
-
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
436
|
-
),
|
|
495
|
+
limits=limits,
|
|
437
496
|
**kwargs,
|
|
438
497
|
)
|
|
439
498
|
|
|
440
|
-
async def _make(self, *args, **kwargs) -> Response:
|
|
499
|
+
async def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
441
500
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
442
501
|
url = ElabFTWURL(
|
|
443
502
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
@@ -448,7 +507,7 @@ class AsyncPATCHRequest(APIRequest, is_async_client=True):
|
|
|
448
507
|
}
|
|
449
508
|
return await super().client.patch(
|
|
450
509
|
url.get(),
|
|
451
|
-
headers={"Accept": "application/json"},
|
|
510
|
+
headers=headers or {"Accept": "application/json"},
|
|
452
511
|
json=data,
|
|
453
512
|
**kwargs,
|
|
454
513
|
)
|
|
@@ -485,13 +544,15 @@ class DELETERequest(APIRequest):
|
|
|
485
544
|
def __init__(self, **kwargs):
|
|
486
545
|
super().__init__(**kwargs)
|
|
487
546
|
|
|
488
|
-
def _make(self, *args) -> Response:
|
|
547
|
+
def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
489
548
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
490
549
|
url = ElabFTWURL(
|
|
491
550
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
492
551
|
)
|
|
493
552
|
return super().client.delete(
|
|
494
|
-
url.get(),
|
|
553
|
+
url.get(),
|
|
554
|
+
headers=headers or {"Accept": "*/*", "Content-Type": "application/json"},
|
|
555
|
+
**kwargs,
|
|
495
556
|
)
|
|
496
557
|
|
|
497
558
|
def close(self):
|
|
@@ -504,32 +565,42 @@ class DELETERequest(APIRequest):
|
|
|
504
565
|
sub_endpoint_name: Optional[str] = None,
|
|
505
566
|
sub_endpoint_id: Union[int, str, None] = None,
|
|
506
567
|
query: Optional[dict] = None,
|
|
568
|
+
**kwargs,
|
|
507
569
|
) -> Response:
|
|
508
570
|
return super().__call__(
|
|
509
|
-
endpoint_name,
|
|
571
|
+
endpoint_name,
|
|
572
|
+
endpoint_id,
|
|
573
|
+
sub_endpoint_name,
|
|
574
|
+
sub_endpoint_id,
|
|
575
|
+
query,
|
|
576
|
+
**kwargs,
|
|
510
577
|
)
|
|
511
578
|
|
|
512
579
|
|
|
513
580
|
class AsyncDELETERequest(APIRequest, is_async_client=True):
|
|
514
581
|
__slots__ = ()
|
|
515
582
|
|
|
516
|
-
def __init__(
|
|
583
|
+
def __init__(
|
|
584
|
+
self,
|
|
585
|
+
limits=Limits(
|
|
586
|
+
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
587
|
+
),
|
|
588
|
+
**kwargs,
|
|
589
|
+
):
|
|
517
590
|
super().__init__(
|
|
518
|
-
|
|
519
|
-
limits=Limits(
|
|
520
|
-
max_connections=100, max_keepalive_connections=30, keepalive_expiry=60
|
|
521
|
-
),
|
|
591
|
+
limits=limits,
|
|
522
592
|
**kwargs,
|
|
523
593
|
)
|
|
524
594
|
|
|
525
|
-
async def _make(self, *args) -> Response:
|
|
595
|
+
async def _make(self, *args, headers: Optional[dict] = None, **kwargs) -> Response:
|
|
526
596
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query = args
|
|
527
597
|
url = ElabFTWURL(
|
|
528
598
|
endpoint_name, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
|
|
529
599
|
)
|
|
530
600
|
return await super().client.delete(
|
|
531
601
|
url.get(),
|
|
532
|
-
headers={"Accept": "*/*", "Content-Type": "application/json"},
|
|
602
|
+
headers=headers or {"Accept": "*/*", "Content-Type": "application/json"},
|
|
603
|
+
**kwargs,
|
|
533
604
|
)
|
|
534
605
|
|
|
535
606
|
async def close(self):
|
|
@@ -543,9 +614,15 @@ class AsyncDELETERequest(APIRequest, is_async_client=True):
|
|
|
543
614
|
sub_endpoint_name: Optional[str] = None,
|
|
544
615
|
sub_endpoint_id: Union[int, str, None] = None,
|
|
545
616
|
query: Optional[dict] = None,
|
|
617
|
+
**kwargs,
|
|
546
618
|
) -> Response:
|
|
547
619
|
response = await self._make(
|
|
548
|
-
endpoint_name,
|
|
620
|
+
endpoint_name,
|
|
621
|
+
endpoint_id,
|
|
622
|
+
sub_endpoint_name,
|
|
623
|
+
sub_endpoint_id,
|
|
624
|
+
query,
|
|
625
|
+
**kwargs,
|
|
549
626
|
)
|
|
550
627
|
if not self.keep_session_open:
|
|
551
628
|
await self.close()
|
|
@@ -1,6 +1,105 @@
|
|
|
1
|
-
from
|
|
1
|
+
from json import JSONDecodeError
|
|
2
|
+
from typing import Union, Iterable, Optional
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from ..core_validators import Validator, RuntimeValidationError, CriticalValidationError
|
|
7
|
+
from ..styles import stdin_console
|
|
8
|
+
from ..styles.highlight import NoteText
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HostIdentityValidator(Validator):
|
|
12
|
+
__slots__ = ()
|
|
13
|
+
|
|
14
|
+
def __init__(self, restrict_to: Union[str, Iterable[str], None] = None):
|
|
15
|
+
self.restrict_to = restrict_to
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def restrict_to(self) -> Iterable[str]:
|
|
19
|
+
return self._restrict_to
|
|
20
|
+
|
|
21
|
+
@restrict_to.setter
|
|
22
|
+
def restrict_to(self, value):
|
|
23
|
+
if value is None:
|
|
24
|
+
self._restrict_to = None
|
|
25
|
+
elif isinstance(value, str):
|
|
26
|
+
self._restrict_to = [value]
|
|
27
|
+
elif isinstance(value, Iterable):
|
|
28
|
+
self._restrict_to = value
|
|
29
|
+
else:
|
|
30
|
+
raise AttributeError(
|
|
31
|
+
"restrict_to must be a string of target host URL, or an iterable of strings where "
|
|
32
|
+
f"each string is a host URL that {HostIdentityValidator.__name__} validation will be restricted to."
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def check_endpoint():
|
|
37
|
+
from ..api import GETRequest
|
|
38
|
+
|
|
39
|
+
session = GETRequest()
|
|
40
|
+
return session(endpoint_name="apikeys", endpoint_id=None)
|
|
41
|
+
|
|
42
|
+
def validate(self):
|
|
43
|
+
from ..loggers import Logger
|
|
44
|
+
from ..configuration import get_active_host, get_active_api_token
|
|
45
|
+
from ..configuration import KEY_HOST
|
|
46
|
+
|
|
47
|
+
logger = Logger()
|
|
48
|
+
host = get_active_host()
|
|
49
|
+
api_token = get_active_api_token()
|
|
50
|
+
if self.restrict_to is not None:
|
|
51
|
+
if host not in self.restrict_to:
|
|
52
|
+
logger.error(
|
|
53
|
+
f"Detected '{KEY_HOST.lower()}' is different from the restricted host. "
|
|
54
|
+
f"'{KEY_HOST.lower()}' could not be validated!"
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
stdin_console.print(
|
|
58
|
+
NoteText(
|
|
59
|
+
f"Detected '{KEY_HOST.lower()}': '{host}'. "
|
|
60
|
+
f"Host(s) restricted by {HostIdentityValidator.__name__}: '{', '.join(self.restrict_to)}'."
|
|
61
|
+
)
|
|
62
|
+
)
|
|
63
|
+
except TypeError as e:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"An invalid value might have been given to restrict_to "
|
|
66
|
+
f"attribute of {HostIdentityValidator.__name__}. Validation could not be completed!"
|
|
67
|
+
) from e
|
|
68
|
+
raise CriticalValidationError
|
|
69
|
+
|
|
70
|
+
API_TOKEN_MASKED = api_token
|
|
71
|
+
try:
|
|
72
|
+
response: httpx.Response = self.check_endpoint()
|
|
73
|
+
response.json()
|
|
74
|
+
except (httpx.HTTPError, JSONDecodeError) as error:
|
|
75
|
+
logger.critical(
|
|
76
|
+
f"There was a problem accessing host '{host}' with API token '{API_TOKEN_MASKED}'."
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
# noinspection PyUnboundLocalVariable
|
|
80
|
+
logger.info(
|
|
81
|
+
f"Returned response: '{response.status_code}: {response.text}'"
|
|
82
|
+
)
|
|
83
|
+
except UnboundLocalError:
|
|
84
|
+
logger.info(
|
|
85
|
+
f"No request was made to the host URL! Exception details: '{error!r}'"
|
|
86
|
+
)
|
|
87
|
+
raise RuntimeValidationError
|
|
88
|
+
if response.is_server_error:
|
|
89
|
+
logger.critical(
|
|
90
|
+
f"There was a problem with the host server: '{host}'. "
|
|
91
|
+
f"Please contact an administrator."
|
|
92
|
+
)
|
|
93
|
+
raise RuntimeValidationError
|
|
94
|
+
stdin_console.print(
|
|
95
|
+
NoteText(
|
|
96
|
+
"There is likely nothing wrong with the host server. "
|
|
97
|
+
"Possible reasons for failure:\n"
|
|
98
|
+
"• Invalid/expired/incorrect API token\n"
|
|
99
|
+
"• Incorrect host URL\n",
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
raise RuntimeValidationError
|
|
4
103
|
|
|
5
104
|
|
|
6
105
|
class PermissionValidator(Validator):
|
|
@@ -63,13 +162,12 @@ class PermissionValidator(Validator):
|
|
|
63
162
|
def validate(self) -> None:
|
|
64
163
|
from ..api import GETRequest
|
|
65
164
|
from ..loggers import Logger
|
|
66
|
-
from .identity import COMMON_NETWORK_ERRORS, HostIdentityValidator
|
|
67
165
|
|
|
68
166
|
logger = Logger()
|
|
69
167
|
try:
|
|
70
168
|
session = GETRequest(keep_session_open=True)
|
|
71
169
|
caller_data: dict = session(endpoint_name="users", endpoint_id="me").json()
|
|
72
|
-
except
|
|
170
|
+
except (httpx.HTTPError, JSONDecodeError):
|
|
73
171
|
logger.critical(
|
|
74
172
|
"Something went wrong while trying to read user information! "
|
|
75
173
|
f"Try to validate the configuration first with '{HostIdentityValidator.__name__}' "
|
|
@@ -111,7 +209,6 @@ class APITokenRWValidator(Validator):
|
|
|
111
209
|
|
|
112
210
|
def validate(self):
|
|
113
211
|
from ..api import GETRequest
|
|
114
|
-
from .identity import COMMON_NETWORK_ERRORS, HostIdentityValidator
|
|
115
212
|
from ..loggers import Logger
|
|
116
213
|
|
|
117
214
|
logger = Logger()
|
|
@@ -121,7 +218,7 @@ class APITokenRWValidator(Validator):
|
|
|
121
218
|
api_token_data: Optional[dict] = _session(
|
|
122
219
|
endpoint_name="apikeys", endpoint_id="me"
|
|
123
220
|
).json()[0]
|
|
124
|
-
except
|
|
221
|
+
except (httpx.HTTPError, JSONDecodeError):
|
|
125
222
|
logger.critical(
|
|
126
223
|
"Something went wrong while trying to read API token information! "
|
|
127
224
|
f"Try to validate the configuration first with '{HostIdentityValidator.__name__}' "
|
elapi/cli/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from ..plugins.commons.cli_helpers import CLIExport, CLIFormat, OrderedCommands # noqa: F401
|
elapi/cli/__main__.py
CHANGED
elapi/cli/_plugin_handler.py
CHANGED
|
@@ -38,7 +38,10 @@ class InternalPluginHandler:
|
|
|
38
38
|
) # Python will find module relative to __package__ path,
|
|
39
39
|
# without this module.__package__ change Python will throw an ImportError.
|
|
40
40
|
spec.loader.exec_module(module)
|
|
41
|
-
|
|
41
|
+
try:
|
|
42
|
+
yield getattr(module, INTERNAL_PLUGIN_TYPER_APP_VAR_NAME)
|
|
43
|
+
except AttributeError:
|
|
44
|
+
yield
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
internal_plugin_typer_apps = InternalPluginHandler().get_typer_apps()
|
elapi/cli/doc.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
This script includes docstring for elapi. The docstrings are mainly meant to be used with a CLI interface.
|
|
3
3
|
"""
|
|
4
|
+
from .._names import CONFIG_FILE_NAME
|
|
4
5
|
from ..configuration import APP_NAME, DEFAULT_EXPORT_DATA_FORMAT, EXPORT_DIR
|
|
5
6
|
from ..styles import BaseFormat
|
|
6
7
|
|
|
@@ -82,10 +83,27 @@ __PARAMETERS__doc__ = {
|
|
|
82
83
|
" to some external formatting program like `less`. "
|
|
83
84
|
"If an unsupported format value is provided then the output format "
|
|
84
85
|
f"falls back to `{DEFAULT_EXPORT_DATA_FORMAT.upper()}`.",
|
|
86
|
+
"verify": "SSL certificates are verified for HTTPS requests by default. Passing _'--verify False'_ will "
|
|
87
|
+
"disable the verification. This can be useful during local development. You can also pass a "
|
|
88
|
+
"path to SSL certificate (a.k.a CA bundle) file to --verify.",
|
|
89
|
+
"timeout": f"Maximum number of seconds {APP_NAME} will wait for response before giving up. "
|
|
90
|
+
f"Default timeout is **5** seconds.",
|
|
91
|
+
"headers": f"{APP_NAME} decides the appropriate headers in most cases. --headers will let you overwrite "
|
|
92
|
+
f"the default headers sent. The value for --headers should be in **JSON** format. E.g., "
|
|
93
|
+
f"`{APP_NAME} get info --headers '{{\"Accept\": \"application/json\", \"User-Agent\": "
|
|
94
|
+
f"\"My custom agent 1.0\"}}'`.",
|
|
85
95
|
"no_keys": "Do not show the names of configuration keywords.",
|
|
86
96
|
"init_host": 'The host URL of your eLabFTW instance. It will look like \"https://demo.elabftw.net/api/v2\".',
|
|
87
97
|
"init_api_token": 'API token (or API key) of your eLabFTW instance. You can generate it from eLabFTW "User Panel". '
|
|
88
98
|
'Make sure your API key has proper permission for your future tasks.',
|
|
89
99
|
"init_export_dir": f"Preferred export directory. If '--export-dir' isn't passed, {EXPORT_DIR} will be "
|
|
90
|
-
"set as the export directory."
|
|
100
|
+
"set as the export directory.",
|
|
101
|
+
"cli_startup": f"⚡️Force override detected configuration from '{CONFIG_FILE_NAME}'. "
|
|
102
|
+
"The value should be in **JSON** format. This option can only be passed "
|
|
103
|
+
"**before** passing any other argument/option/command. E.g., "
|
|
104
|
+
'`elapi --OC \'{"timeout": "10", "verify_ssl": "false"}\' get info -F yml`. '
|
|
105
|
+
'You can use tools like [yq](https://mikefarah.gitbook.io/yq) and '
|
|
106
|
+
'[jq](https://jqlang.github.io/jq/) to read values from YAML and JSON _files_ respectively, '
|
|
107
|
+
'if you do not wish to hand-type JSON syntax. E.g., '
|
|
108
|
+
'`elapi --OC "$(cat ~/params.yml | yq e -o json -I0)" experiments get -i <experiment ID> -F csv`.',
|
|
91
109
|
}
|