elapi 1.0.7.dev7__py3-none-any.whl → 2.0.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.
elapi/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.7.dev7
1
+ 2.0.0
elapi/_names.py CHANGED
@@ -5,8 +5,10 @@ from pathlib import Path
5
5
  # In which case, import their counterparts from src/config.py
6
6
  # name definitions
7
7
  APP_NAME: str = "elapi"
8
+ APP_BRAND_NAME: str = "elAPI"
8
9
  LOG_FILE_NAME: str = f"{APP_NAME}.log"
9
- CONFIG_FILE_NAME: str = f"{APP_NAME}.yml"
10
+ CONFIG_FILE_EXTENSION: str = "yml"
11
+ CONFIG_FILE_NAME: str = f"{APP_NAME}.{CONFIG_FILE_EXTENSION}"
10
12
  DEFAULT_EXPORT_DATA_FORMAT: str = "json"
11
13
  user_home: Path = Path.home()
12
14
  cur_dir: Path = Path.cwd()
@@ -41,6 +43,7 @@ KEY_UNSAFE_TOKEN_WARNING: str = "UNSAFE_API_TOKEN_WARNING"
41
43
  KEY_ENABLE_HTTP2: str = "ENABLE_HTTP2"
42
44
  KEY_VERIFY_SSL: str = "VERIFY_SSL"
43
45
  KEY_TIMEOUT: str = "TIMEOUT"
46
+ KEY_DEVELOPMENT_MODE: str = "DEVELOPMENT_MODE"
44
47
 
45
48
  # Log data directory with root permission
46
49
  LOG_DIR_ROOT: Path = Path(f"/var/log/{APP_NAME}")
elapi/api/api.py CHANGED
@@ -5,6 +5,7 @@ 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 .. import APP_NAME
8
9
  from ..configuration import (
9
10
  TOKEN_BEARER,
10
11
  get_active_host,
@@ -19,6 +20,9 @@ class APIRequest(ABC):
19
20
  __slots__ = "keep_session_open", "_client", "host", "api_token", "header_name"
20
21
 
21
22
  def __init__(self, keep_session_open: bool = False, **kwargs):
23
+ from ..utils import check_reserved_keyword
24
+ from .._names import CONFIG_FILE_NAME
25
+
22
26
  self.host: str = get_active_host()
23
27
  self.api_token: str = get_active_api_token().token
24
28
  self.header_name: str = TOKEN_BEARER
@@ -27,13 +31,23 @@ class APIRequest(ABC):
27
31
  verify_ssl = get_active_verify_ssl()
28
32
  timeout = get_active_timeout()
29
33
  _client = Client if not self.is_async_client else AsyncClient
30
- self._client: Union[Client, AsyncClient] = _client(
31
- auth=HeaderApiKey(api_key=self.api_token, header_name=self.header_name),
32
- http2=enable_http2,
33
- verify=verify_ssl,
34
- timeout=timeout,
35
- **kwargs,
36
- )
34
+ try:
35
+ self._client: Union[Client, AsyncClient] = _client(
36
+ auth=HeaderApiKey(api_key=self.api_token, header_name=self.header_name),
37
+ http2=enable_http2,
38
+ verify=verify_ssl,
39
+ timeout=timeout,
40
+ **kwargs,
41
+ )
42
+ except TypeError as e:
43
+ check_reserved_keyword(
44
+ e,
45
+ what=f"{APP_NAME}",
46
+ against=f"abstract class {APIRequest.__name__}, "
47
+ f"so the parameter remains user-configurable "
48
+ f"through {CONFIG_FILE_NAME} configuration file",
49
+ )
50
+ raise e
37
51
 
38
52
  # noinspection PyMethodOverriding
39
53
  def __init_subclass__(cls, /, is_async_client: bool = False, **kwargs):
@@ -1,17 +1,30 @@
1
1
  import importlib.util
2
2
  import sys
3
+ from collections import namedtuple
3
4
  from pathlib import Path
4
- from typing import List, Tuple, Generator
5
+ from typing import List, Tuple, Generator, Union, Optional
5
6
 
6
7
  import typer
7
8
 
9
+ from ..configuration import get_development_mode
8
10
  from ..configuration.config import (
9
11
  ROOT_INSTALLATION_DIR,
10
12
  INTERNAL_PLUGIN_DIRECTORY_NAME,
11
13
  INTERNAL_PLUGIN_TYPER_APP_FILE_NAME_PREFIX,
12
14
  INTERNAL_PLUGIN_TYPER_APP_FILE_NAME,
13
15
  INTERNAL_PLUGIN_TYPER_APP_VAR_NAME,
16
+ EXTERNAL_LOCAL_PLUGIN_DIRECTORY_NAME,
17
+ EXTERNAL_LOCAL_PLUGIN_DIR,
18
+ EXTERNAL_LOCAL_PLUGIN_TYPER_APP_FILE_NAME_PREFIX,
19
+ EXTERNAL_LOCAL_PLUGIN_TYPER_APP_VAR_NAME,
14
20
  )
21
+ from ..core_validators import Validator, ValidationError, Validate
22
+ from ..loggers import Logger
23
+ from ..path import ProperPath
24
+ from ..utils import add_message
25
+
26
+ logger = Logger()
27
+ PluginInfo = namedtuple("PluginInfo", ["plugin_app", "path", "venv"])
15
28
 
16
29
 
17
30
  class InternalPluginHandler:
@@ -44,4 +57,335 @@ class InternalPluginHandler:
44
57
  yield
45
58
 
46
59
 
60
+ class ExternalPluginLocationValidator(Validator):
61
+ def __init__(self, location: Union[str, Path, ProperPath], /):
62
+ self.location = location
63
+
64
+ @property
65
+ def location(self):
66
+ return self._location
67
+
68
+ @location.setter
69
+ def location(self, value):
70
+ if not isinstance(value, ProperPath):
71
+ try:
72
+ value = ProperPath(value)
73
+ except ValueError as e:
74
+ raise ValueError(
75
+ f"'location' attribute for class {self.__class__.__class__} "
76
+ f"is invalid."
77
+ ) from e
78
+ else:
79
+ self._location = value.expanded
80
+
81
+ def validate(self):
82
+ import os
83
+ import yaml
84
+ from ..configuration import (
85
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_NAME,
86
+ EXTERNAL_LOCAL_PLUGIN_TYPER_APP_FILE_NAME,
87
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS,
88
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH,
89
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH,
90
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME,
91
+ CANON_YAML_EXTENSION,
92
+ )
93
+
94
+ parsed_metadata: dict = {
95
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS: None,
96
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH: None,
97
+ # EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH: None,
98
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME: None,
99
+ }
100
+
101
+ if self.location.is_dir():
102
+ actual_cwd = Path.cwd()
103
+ os.chdir(self.location)
104
+ if (
105
+ plugin_metadata_file := (
106
+ self.location / EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_NAME
107
+ )
108
+ ).exists():
109
+ parsed_metadata[EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS] = (
110
+ True
111
+ )
112
+ with ProperPath(plugin_metadata_file).open(mode="r") as f:
113
+ try:
114
+ plugin_metadata = yaml.safe_load(f)
115
+ except yaml.YAMLError as e:
116
+ raise ValidationError(
117
+ f"Plugin {CANON_YAML_EXTENSION.upper()} "
118
+ f"metadata file {plugin_metadata_file} exists, "
119
+ f"but it couldn't be parsed. Exception details: {e}"
120
+ )
121
+ else:
122
+ try:
123
+ CLI_SCRIPT_PATH = plugin_metadata[
124
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH
125
+ ]
126
+ except KeyError:
127
+ if (
128
+ external_local_plugin_typer_app_file := (
129
+ self.location
130
+ / EXTERNAL_LOCAL_PLUGIN_TYPER_APP_FILE_NAME
131
+ )
132
+ ).exists():
133
+ parsed_metadata[
134
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH
135
+ ] = external_local_plugin_typer_app_file
136
+ else:
137
+ try:
138
+ CLI_SCRIPT_PATH = ProperPath(CLI_SCRIPT_PATH)
139
+ except ValueError:
140
+ raise ValidationError(
141
+ f"Key '{EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH}' "
142
+ f"exists in {plugin_metadata_file}, but its assigned "
143
+ f"value '{CLI_SCRIPT_PATH}' is invalid."
144
+ )
145
+ else:
146
+ if CLI_SCRIPT_PATH.expanded.exists():
147
+ parsed_metadata[
148
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH
149
+ ] = CLI_SCRIPT_PATH.expanded.absolute()
150
+ else:
151
+ raise ValidationError(
152
+ f"Key '{EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH}' "
153
+ f"exists in {plugin_metadata_file}, but the path "
154
+ f"{CLI_SCRIPT_PATH} does not exist."
155
+ )
156
+ try:
157
+ VENV_PATH = plugin_metadata[
158
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH
159
+ ]
160
+ except KeyError:
161
+ VENV_PATH = parsed_metadata[
162
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH
163
+ ] = None
164
+ else:
165
+ try:
166
+ VENV_PATH = ProperPath(VENV_PATH)
167
+ except ValueError:
168
+ raise ValidationError(
169
+ f"Key '{EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH}' "
170
+ f"exists in {plugin_metadata_file}, but its assigned "
171
+ f"value '{VENV_PATH}' is invalid."
172
+ )
173
+ else:
174
+ if VENV_PATH.expanded.exists():
175
+ parsed_metadata[
176
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH
177
+ ] = VENV_PATH.expanded.absolute()
178
+ else:
179
+ raise ValidationError(
180
+ f"Key '{EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH}' "
181
+ f"exists in {plugin_metadata_file}, but the path "
182
+ f"{VENV_PATH} does not exist."
183
+ )
184
+ try:
185
+ PLUGIN_NAME = plugin_metadata[
186
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME
187
+ ]
188
+ except KeyError:
189
+ PLUGIN_NAME = parsed_metadata[
190
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME
191
+ ] = self.location.name
192
+ else:
193
+ if self.location.name != PLUGIN_NAME:
194
+ raise ValidationError(
195
+ f"Key '{EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME}' "
196
+ f"exists in {plugin_metadata_file}, but it must be the same "
197
+ f"name as the directory name the metadata file is in."
198
+ )
199
+ parsed_metadata[
200
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME
201
+ ] = PLUGIN_NAME
202
+ else:
203
+ parsed_metadata[EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS] = (
204
+ False
205
+ )
206
+ if (
207
+ external_local_plugin_typer_app_file := (
208
+ self.location / EXTERNAL_LOCAL_PLUGIN_TYPER_APP_FILE_NAME
209
+ )
210
+ ).exists():
211
+ parsed_metadata[
212
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH
213
+ ] = external_local_plugin_typer_app_file
214
+ PLUGIN_NAME = self.location.name
215
+ parsed_metadata[
216
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME
217
+ ] = PLUGIN_NAME
218
+ else:
219
+ raise ValueError(f"{self.location} may not be a plugin directory.")
220
+ os.chdir(actual_cwd)
221
+ else:
222
+ raise ValueError(f"{self.location} is not a directory.")
223
+ return parsed_metadata
224
+
225
+
226
+ class ExternalPluginHandler:
227
+ @staticmethod
228
+ def get_plugin_metadata() -> Generator[Optional[dict], None, None]:
229
+ from ..utils import add_message
230
+ from ..configuration import EXTERNAL_LOCAL_PLUGIN_METADATA_KEY_PLUGIN_ROOT_DIR
231
+
232
+ try:
233
+ for path in sorted(
234
+ EXTERNAL_LOCAL_PLUGIN_DIR.iterdir(), key=lambda x: str(x).lower()
235
+ ):
236
+ try:
237
+ metadata = Validate(ExternalPluginLocationValidator(path)).get()
238
+ except ValidationError as e:
239
+ add_message(f"{e}")
240
+ continue
241
+ except ValueError:
242
+ continue
243
+ else:
244
+ metadata[EXTERNAL_LOCAL_PLUGIN_METADATA_KEY_PLUGIN_ROOT_DIR] = path
245
+ yield metadata
246
+ except FileNotFoundError:
247
+ yield None
248
+
249
+ @staticmethod
250
+ def load_plugin(plugin_name: str, cli_script: Path):
251
+ spec = importlib.util.spec_from_file_location(plugin_name, cli_script)
252
+ module = importlib.util.module_from_spec(spec)
253
+ sys.modules[spec.name] = module
254
+ module.__package__ = (
255
+ f"{__package__.removesuffix(f'.{EXTERNAL_LOCAL_PLUGIN_TYPER_APP_FILE_NAME_PREFIX}')}"
256
+ f".{EXTERNAL_LOCAL_PLUGIN_DIRECTORY_NAME}.{plugin_name}"
257
+ ) # Python will find module relative to __package__ path,
258
+ # without this module.__package__ change Python will throw an ImportError.
259
+ spec.loader.exec_module(module)
260
+ return module
261
+
262
+ def get_typer_apps(self) -> Generator[PluginInfo, None, None]:
263
+ import logging
264
+ from ..configuration import (
265
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS,
266
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH,
267
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME,
268
+ EXTERNAL_LOCAL_PLUGIN_METADATA_KEY_PLUGIN_ROOT_DIR,
269
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH,
270
+ )
271
+ from ._venv_state_manager import switch_venv_state
272
+
273
+ for metadata in self.get_plugin_metadata():
274
+ if metadata is None:
275
+ break
276
+ plugin_name: str = metadata[
277
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_PLUGIN_NAME
278
+ ]
279
+ cli_script: Path = metadata[
280
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_CLI_SCRIPT_PATH
281
+ ]
282
+ plugin_root_dir: Path = metadata[
283
+ EXTERNAL_LOCAL_PLUGIN_METADATA_KEY_PLUGIN_ROOT_DIR
284
+ ]
285
+ if metadata[EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_FILE_EXISTS] is False:
286
+ try:
287
+ module = self.load_plugin(plugin_name, cli_script)
288
+ except (Exception, BaseException) as e:
289
+ if get_development_mode() is True:
290
+ raise e
291
+ message: str = (
292
+ f"An exception occurred while trying to load a local "
293
+ f"plugin '{plugin_name}' in path {cli_script}. "
294
+ f"Plugin '{plugin_name}' will be ignored. "
295
+ f'Exception details: "{e.__class__.__name__}: {e}"'
296
+ )
297
+ add_message(message, logging.WARNING)
298
+ yield
299
+ else:
300
+ try:
301
+ typer_app: typer.Typer = getattr(
302
+ module, EXTERNAL_LOCAL_PLUGIN_TYPER_APP_VAR_NAME
303
+ )
304
+ except AttributeError:
305
+ yield
306
+ else:
307
+ typer_app.info.name = plugin_name
308
+ yield PluginInfo(
309
+ typer_app,
310
+ plugin_root_dir,
311
+ None,
312
+ )
313
+ else:
314
+ venv_dir: Path = metadata[
315
+ EXTERNAL_LOCAL_PLUGIN_METADATA_FILE_KEY_VENV_PATH
316
+ ]
317
+ if venv_dir is not None:
318
+ try:
319
+ switch_venv_state(True, venv_dir)
320
+ except (ValueError, RuntimeError) as e:
321
+ message: str = (
322
+ f"An exception occurred while trying to load a local "
323
+ f"plugin '{plugin_name}' with virtual environment {venv_dir} "
324
+ f"in path {cli_script}. "
325
+ f"Plugin '{plugin_name}' will be ignored. "
326
+ f'Exception details: "{e.__class__.__name__}: {e}"'
327
+ )
328
+ add_message(message, logging.WARNING)
329
+ yield
330
+ else:
331
+ try:
332
+ module = self.load_plugin(plugin_name, cli_script)
333
+ except (Exception, BaseException) as e:
334
+ if get_development_mode() is True:
335
+ raise e
336
+ message: str = (
337
+ f"An exception occurred while trying to load a local "
338
+ f"plugin '{plugin_name}' with virtual environment {venv_dir} "
339
+ f"in path {cli_script}. "
340
+ f"Plugin '{plugin_name}' will be ignored. "
341
+ f'Exception details: "{e.__class__.__name__}: {e}"'
342
+ )
343
+ add_message(message, logging.WARNING)
344
+ yield
345
+ else:
346
+ try:
347
+ typer_app: typer.Typer = getattr(
348
+ module, EXTERNAL_LOCAL_PLUGIN_TYPER_APP_VAR_NAME
349
+ )
350
+ except AttributeError:
351
+ yield
352
+ else:
353
+ switch_venv_state(False, venv_dir)
354
+ typer_app.info.name = plugin_name
355
+ yield PluginInfo(
356
+ typer_app,
357
+ plugin_root_dir,
358
+ venv_dir,
359
+ )
360
+ else:
361
+ try:
362
+ module = self.load_plugin(plugin_name, cli_script)
363
+ except (Exception, BaseException) as e:
364
+ if get_development_mode() is True:
365
+ raise e
366
+ message: str = (
367
+ f"An exception occurred while trying to load a local "
368
+ f"plugin '{plugin_name}' in path {cli_script}. "
369
+ f"Plugin '{plugin_name}' will be ignored. "
370
+ f'Exception details: "{e.__class__.__name__}: {e}"'
371
+ )
372
+ add_message(message, logging.WARNING)
373
+ yield
374
+ else:
375
+ try:
376
+ typer_app: typer.Typer = getattr(
377
+ module, EXTERNAL_LOCAL_PLUGIN_TYPER_APP_VAR_NAME
378
+ )
379
+ except AttributeError:
380
+ yield
381
+ else:
382
+ typer_app.info.name = plugin_name
383
+ yield PluginInfo(
384
+ typer_app,
385
+ plugin_root_dir,
386
+ None,
387
+ )
388
+
389
+
47
390
  internal_plugin_typer_apps = InternalPluginHandler().get_typer_apps()
391
+ external_local_plugin_typer_apps = ExternalPluginHandler().get_typer_apps()
@@ -0,0 +1,39 @@
1
+ from pathlib import Path
2
+ from typing import Union
3
+
4
+ from ..path import ProperPath
5
+
6
+ VENV_INDICATOR_DIR_NAME: str = "site-packages"
7
+
8
+
9
+ def switch_venv_state(status: bool, /, venv_dir: Union[Path, ProperPath]):
10
+ import sys
11
+
12
+ site_packages = sorted(
13
+ venv_dir.rglob(VENV_INDICATOR_DIR_NAME), key=lambda x: str(x).lower()
14
+ )
15
+
16
+ if not site_packages:
17
+ raise ValueError(
18
+ f"Could not find '{VENV_INDICATOR_DIR_NAME}' directory in "
19
+ "virtual environment path."
20
+ )
21
+ for unique_dir in site_packages:
22
+ unique_dir = str(unique_dir)
23
+ if status is True:
24
+ sys.path.insert(1, unique_dir)
25
+ return
26
+ else:
27
+ if sys.path[1] == unique_dir:
28
+ sys.path.pop(1)
29
+ return
30
+ else:
31
+ try:
32
+ sys.path.remove(unique_dir)
33
+ except ValueError as e:
34
+ raise RuntimeError(
35
+ f"Virtual environment directory "
36
+ f"{unique_dir} couldn't be removed from sys.path!"
37
+ ) from e
38
+ else:
39
+ return
elapi/cli/doc.py CHANGED
@@ -29,7 +29,8 @@ __PARAMETERS__doc__ = {
29
29
  "'10' is the sub-endpoint ID of sub-endpoint 'uploads' of the API URL "
30
30
  "_'https://demo.elabftw.net/api/v2/experiments/1/uploads/`10`/'_.",
31
31
  "query": "HTTP query to pass to API URL.\n"
32
- "- The value (parameters) for --query should be in **JSON** format. "
32
+ "- The value (parameters) for --query can be a string in **JSON** format, or "
33
+ "a JSON or YAML **file path**. "
33
34
  f"E.g., `{APP_NAME} get experiments <experiment ID> --query '{{\"format\": \"csv\"}}'` "
34
35
  f"will make a `GET` request to "
35
36
  f"_'https://demo.elabftw.net/api/v2/experiments/\<experiment ID\>/?format=csv'_ "
@@ -42,14 +43,17 @@ __PARAMETERS__doc__ = {
42
43
  # f"`{APP_NAME} post teams -d '{{\"name\": \"Alpha\"}}'`, 2. As regular options. E.g., "
43
44
  # f"`{APP_NAME} post teams --name Alpha`.",
44
45
  "data": f"HTTP POST data. This works similar to how data is passed to `curl`. E.g., "
45
- f"`{APP_NAME} post teams -d '{{\"name\": \"Alpha\"}}'`,",
46
- "file_post": "Send a file with a request. The value for --file should be in **JSON** format. The value must follow "
46
+ f"`{APP_NAME} post teams -d '{{\"name\": \"Alpha\"}}'`. Instead of a JSON string, "
47
+ f"a JSON or YAML **file path** can also be passed to --data/-d.",
48
+ "file_post": "Send a file with a request. The value for --file can be a string in **JSON** format, or "
49
+ "a JSON or YAML **file path**. The value must follow "
47
50
  "one of the following structures: `'{\"file\": \"<file path>\"}'`, "
48
51
  "`'{\"file\": \"<file path>\", \"comment\": \"<file comment>\"}'`, "
49
52
  "`'{\"file\": [\"<file new name>\", \"<file path>\"]}'`, or "
50
53
  "`'{\"file\": [\"<file new name>\", \"<file path>\"], \"comment\": \"<file comment>\"}'`.",
51
54
  "data_patch": f"Modified data to be sent as HTTP PATCH data. This works similar to how data is passed to `curl`. "
52
- f'E.g., `{APP_NAME} patch teams --id <team id> -d \'{{"name": "New team name"}}\'`,',
55
+ f'E.g., `{APP_NAME} patch teams --id <team id> -d \'{{"name": "New team name"}}\'`. '
56
+ f'Instead of a JSON string, a JSON or YAML **file path** can also be passed to --data/-d.',
53
57
  "get_loc": "When _--get-loc_ is passed, if the request is successful, instead of printing the success message, "
54
58
  f"{APP_NAME} returns the ID and the URL (separated by comma) of the newly created resource that can "
55
59
  f"be used to just modify or do automation with the resource later on. The ID and the URL can be "
@@ -89,7 +93,8 @@ __PARAMETERS__doc__ = {
89
93
  "timeout": f"Maximum number of seconds {APP_NAME} will wait for response before giving up. "
90
94
  f"Default timeout is **5** seconds.",
91
95
  "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., "
96
+ f"the default headers sent. The value for --headers can be a string in **JSON** format, "
97
+ f"or a JSON or YAML **file path**. E.g., "
93
98
  f"`{APP_NAME} get info --headers '{{\"Accept\": \"application/json\", \"User-Agent\": "
94
99
  f"\"My custom agent 1.0\"}}'`.",
95
100
  "no_keys": "Do not show the names of configuration keywords.",
@@ -99,11 +104,9 @@ __PARAMETERS__doc__ = {
99
104
  "init_export_dir": f"Preferred export directory. If '--export-dir' isn't passed, {EXPORT_DIR} will be "
100
105
  "set as the export directory.",
101
106
  "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`.',
107
+ "The value can be in **JSON** format as a string, or a JSON or YAML **file path**. "
108
+ "This option can only be passed **before** passing any other "
109
+ "argument/option/command. E.g., "
110
+ '`elapi --OC \'{"timeout": "10", "verify_ssl": "false"}\' get info -F yml`, '
111
+ 'or `elapi --OC "~/.quick-config.yml" get info -F yml`.',
109
112
  }