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 +1 -1
- elapi/_names.py +4 -1
- elapi/api/api.py +21 -7
- elapi/cli/_plugin_handler.py +345 -1
- elapi/cli/_venv_state_manager.py +39 -0
- elapi/cli/doc.py +15 -12
- elapi/cli/elapi.py +338 -167
- elapi/configuration/__init__.py +19 -2
- elapi/configuration/_config_history.py +1 -0
- elapi/configuration/_overload_history.py +145 -0
- elapi/configuration/config.py +52 -2
- elapi/configuration/overridable_vars.py +7 -0
- elapi/configuration/validators.py +17 -83
- elapi/loggers/__init__.py +2 -1
- elapi/loggers/base.py +58 -0
- elapi/path.py +1 -1
- elapi/plugins/commons/__init__.py +2 -1
- elapi/plugins/commons/cli_helpers.py +7 -14
- elapi/plugins/commons/get_data_from_input_or_path.py +173 -0
- elapi/plugins/experiments/_doc.py +2 -1
- elapi/plugins/show_config.py +30 -7
- elapi/styles/__init__.py +1 -0
- elapi/styles/rich_utils.py +17 -0
- elapi/utils/__init__.py +2 -0
- elapi/utils/messages.py +84 -0
- elapi/utils/utils.py +22 -0
- {elapi-1.0.7.dev7.dist-info → elapi-2.0.0.dist-info}/METADATA +93 -30
- {elapi-1.0.7.dev7.dist-info → elapi-2.0.0.dist-info}/RECORD +31 -25
- elapi/_exceptions.py +0 -1
- {elapi-1.0.7.dev7.dist-info → elapi-2.0.0.dist-info}/LICENSE +0 -0
- {elapi-1.0.7.dev7.dist-info → elapi-2.0.0.dist-info}/WHEEL +0 -0
- {elapi-1.0.7.dev7.dist-info → elapi-2.0.0.dist-info}/entry_points.txt +0 -0
elapi/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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):
|
elapi/cli/_plugin_handler.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
103
|
-
"**before** passing any other
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
}
|