elapi 1.0.7.dev4__py3-none-any.whl → 1.0.7.dev6__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 (40) hide show
  1. elapi/VERSION +1 -1
  2. elapi/_names.py +3 -0
  3. elapi/api/__init__.py +15 -0
  4. elapi/{api.py → api/api.py} +130 -53
  5. elapi/{validators/permission.py → api/validators.py} +103 -6
  6. elapi/cli/__init__.py +0 -1
  7. elapi/cli/__main__.py +2 -1
  8. elapi/cli/_plugin_handler.py +4 -1
  9. elapi/cli/doc.py +19 -1
  10. elapi/cli/elapi.py +406 -100
  11. elapi/configuration/__init__.py +20 -0
  12. elapi/configuration/_config_history.py +49 -3
  13. elapi/configuration/config.py +63 -81
  14. elapi/configuration/log_file.py +1 -1
  15. elapi/configuration/overridable_vars.py +45 -0
  16. elapi/configuration/validators.py +556 -0
  17. elapi/{validators → core_validators}/__init__.py +0 -2
  18. elapi/plugins/bill_teams/bill_teams.py +4 -4
  19. elapi/plugins/bill_teams/cli.py +472 -349
  20. elapi/plugins/bill_teams/generate_table.py +13 -0
  21. elapi/plugins/bill_teams/specification.py +47 -0
  22. elapi/plugins/bill_teams/{validator.py → validators.py} +168 -32
  23. elapi/plugins/commons/__init__.py +1 -0
  24. elapi/plugins/commons/cli_helpers.py +36 -3
  25. elapi/plugins/commons/export.py +5 -4
  26. elapi/plugins/commons/get_information.py +2 -2
  27. elapi/plugins/experiments/cli.py +19 -21
  28. elapi/plugins/experiments/experiments.py +2 -2
  29. elapi/plugins/show_config.py +87 -13
  30. elapi/validators.py +24 -0
  31. {elapi-1.0.7.dev4.dist-info → elapi-1.0.7.dev6.dist-info}/METADATA +34 -5
  32. elapi-1.0.7.dev6.dist-info/RECORD +63 -0
  33. elapi/validators/identity.py +0 -163
  34. elapi-1.0.7.dev4.dist-info/RECORD +0 -58
  35. /elapi/{endpoint.py → api/endpoint.py} +0 -0
  36. /elapi/{validators → core_validators}/base.py +0 -0
  37. /elapi/{validators → core_validators}/path.py +0 -0
  38. {elapi-1.0.7.dev4.dist-info → elapi-1.0.7.dev6.dist-info}/LICENSE +0 -0
  39. {elapi-1.0.7.dev4.dist-info → elapi-1.0.7.dev6.dist-info}/WHEEL +0 -0
  40. {elapi-1.0.7.dev4.dist-info → elapi-1.0.7.dev6.dist-info}/entry_points.txt +0 -0
elapi/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.7.dev4
1
+ 1.0.7.dev6
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
@@ -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 .configuration import API_TOKEN, TOKEN_BEARER, HOST
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
- verify=True,
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 ElabFTWURL.VALID_ENDPOINTS[self.endpoint_name]:
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(ElabFTWURL.VALID_ENDPOINTS[self.endpoint_name])}."
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"{ElabFTWURL.host}/{self.endpoint_name}/{self.endpoint_id}/"
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(url.get(), headers={"Accept": "application/json"})
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, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
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__(self, **kwargs):
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
- timeout=180,
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__(self, **kwargs):
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
- timeout=180,
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, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
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__(self, **kwargs):
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
- timeout=180,
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(), headers={"Accept": "*/*", "Content-Type": "application/json"}
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, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
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__(self, **kwargs):
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
- timeout=180,
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, endpoint_id, sub_endpoint_name, sub_endpoint_id, query
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 typing import Union, Optional
1
+ from json import JSONDecodeError
2
+ from typing import Union, Iterable, Optional
2
3
 
3
- from .base import Validator, RuntimeValidationError, CriticalValidationError
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 COMMON_NETWORK_ERRORS:
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 COMMON_NETWORK_ERRORS:
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
@@ -1,3 +1,4 @@
1
1
  from .elapi import app
2
+ from .. import APP_NAME
2
3
 
3
- app(prog_name="elapi")
4
+ app(prog_name=APP_NAME)
@@ -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
- yield getattr(module, INTERNAL_PLUGIN_TYPER_APP_VAR_NAME)
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
  }