meerschaum 2.2.5.dev0__py3-none-any.whl → 2.2.5.dev3__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.
- meerschaum/_internal/arguments/_parse_arguments.py +23 -14
- meerschaum/_internal/arguments/_parser.py +4 -2
- meerschaum/_internal/docs/index.py +31 -125
- meerschaum/_internal/shell/Shell.py +0 -3
- meerschaum/actions/bootstrap.py +14 -235
- meerschaum/actions/edit.py +98 -15
- meerschaum/actions/show.py +13 -4
- meerschaum/actions/stack.py +12 -12
- meerschaum/actions/uninstall.py +24 -29
- meerschaum/api/__init__.py +0 -1
- meerschaum/api/dash/__init__.py +0 -1
- meerschaum/api/dash/callbacks/custom.py +1 -1
- meerschaum/api/dash/plugins.py +5 -6
- meerschaum/config/__init__.py +16 -6
- meerschaum/config/_version.py +1 -1
- meerschaum/core/Pipe/_fetch.py +25 -21
- meerschaum/core/Pipe/_sync.py +89 -59
- meerschaum/plugins/bootstrap.py +333 -0
- meerschaum/utils/formatting/__init__.py +22 -10
- meerschaum/utils/prompt.py +11 -4
- meerschaum/utils/warnings.py +1 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/METADATA +1 -1
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/RECORD +29 -29
- meerschaum/actions/backup.py +0 -43
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/WHEEL +0 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/zip-safe +0 -0
meerschaum/core/Pipe/_sync.py
CHANGED
@@ -34,29 +34,29 @@ class InferFetch:
|
|
34
34
|
MRSM_INFER_FETCH: bool = True
|
35
35
|
|
36
36
|
def sync(
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
37
|
+
self,
|
38
|
+
df: Union[
|
39
|
+
pd.DataFrame,
|
40
|
+
Dict[str, List[Any]],
|
41
|
+
List[Dict[str, Any]],
|
42
|
+
InferFetch
|
43
|
+
] = InferFetch,
|
44
|
+
begin: Union[datetime, int, str, None] = '',
|
45
|
+
end: Union[datetime, int, None] = None,
|
46
|
+
force: bool = False,
|
47
|
+
retries: int = 10,
|
48
|
+
min_seconds: int = 1,
|
49
|
+
check_existing: bool = True,
|
50
|
+
blocking: bool = True,
|
51
|
+
workers: Optional[int] = None,
|
52
|
+
callback: Optional[Callable[[Tuple[bool, str]], Any]] = None,
|
53
|
+
error_callback: Optional[Callable[[Exception], Any]] = None,
|
54
|
+
chunksize: Optional[int] = -1,
|
55
|
+
sync_chunks: bool = True,
|
56
|
+
debug: bool = False,
|
57
|
+
_inplace: bool = True,
|
58
|
+
**kw: Any
|
59
|
+
) -> SuccessTuple:
|
60
60
|
"""
|
61
61
|
Fetch new data from the source and update the pipe's table with new data.
|
62
62
|
|
@@ -125,7 +125,7 @@ def sync(
|
|
125
125
|
from meerschaum.utils.formatting import get_console
|
126
126
|
from meerschaum.utils.venv import Venv
|
127
127
|
from meerschaum.connectors import get_connector_plugin
|
128
|
-
from meerschaum.utils.misc import df_is_chunk_generator
|
128
|
+
from meerschaum.utils.misc import df_is_chunk_generator, filter_keywords
|
129
129
|
from meerschaum.utils.pool import get_pool
|
130
130
|
from meerschaum.config import get_config
|
131
131
|
|
@@ -186,7 +186,7 @@ def sync(
|
|
186
186
|
### use that instead.
|
187
187
|
### NOTE: The DataFrame must be omitted for the plugin sync method to apply.
|
188
188
|
### If a DataFrame is provided, continue as expected.
|
189
|
-
if hasattr(df, 'MRSM_INFER_FETCH'):
|
189
|
+
if hasattr(df, 'MRSM_INFER_FETCH'):
|
190
190
|
try:
|
191
191
|
if p.connector is None:
|
192
192
|
msg = f"{p} does not have a valid connector."
|
@@ -194,7 +194,7 @@ def sync(
|
|
194
194
|
msg += f"\n Perhaps {p.connector_keys} has a syntax error?"
|
195
195
|
p._exists = None
|
196
196
|
return False, msg
|
197
|
-
except Exception
|
197
|
+
except Exception:
|
198
198
|
p._exists = None
|
199
199
|
return False, f"Unable to create the connector for {p}."
|
200
200
|
|
@@ -210,14 +210,28 @@ def sync(
|
|
210
210
|
):
|
211
211
|
with Venv(get_connector_plugin(self.instance_connector)):
|
212
212
|
p._exists = None
|
213
|
-
return self.instance_connector.sync_pipe_inplace(
|
213
|
+
return self.instance_connector.sync_pipe_inplace(
|
214
|
+
p,
|
215
|
+
**filter_keywords(
|
216
|
+
p.instance_connector.sync_pipe_inplace,
|
217
|
+
debug=debug,
|
218
|
+
**kw
|
219
|
+
)
|
220
|
+
)
|
214
221
|
|
215
222
|
|
216
223
|
### Activate and invoke `sync(pipe)` for plugin connectors with `sync` methods.
|
217
224
|
try:
|
218
225
|
if getattr(p.connector, 'sync', None) is not None:
|
219
226
|
with Venv(get_connector_plugin(p.connector), debug=debug):
|
220
|
-
return_tuple = p.connector.sync(
|
227
|
+
return_tuple = p.connector.sync(
|
228
|
+
p,
|
229
|
+
**filter_keywords(
|
230
|
+
p.connector.sync,
|
231
|
+
debug=debug,
|
232
|
+
**kw
|
233
|
+
)
|
234
|
+
)
|
221
235
|
p._exists = None
|
222
236
|
if not isinstance(return_tuple, tuple):
|
223
237
|
return_tuple = (
|
@@ -237,13 +251,19 @@ def sync(
|
|
237
251
|
### Fetch the dataframe from the connector's `fetch()` method.
|
238
252
|
try:
|
239
253
|
with Venv(get_connector_plugin(p.connector), debug=debug):
|
240
|
-
df = p.fetch(
|
254
|
+
df = p.fetch(
|
255
|
+
**filter_keywords(
|
256
|
+
p.fetch,
|
257
|
+
debug=debug,
|
258
|
+
**kw
|
259
|
+
)
|
260
|
+
)
|
241
261
|
|
242
262
|
except Exception as e:
|
243
263
|
get_console().print_exception(
|
244
|
-
suppress
|
245
|
-
'meerschaum/core/Pipe/_sync.py',
|
246
|
-
'meerschaum/core/Pipe/_fetch.py',
|
264
|
+
suppress=[
|
265
|
+
'meerschaum/core/Pipe/_sync.py',
|
266
|
+
'meerschaum/core/Pipe/_fetch.py',
|
247
267
|
]
|
248
268
|
)
|
249
269
|
msg = f"Failed to fetch data from {p.connector}:\n {e}"
|
@@ -289,7 +309,7 @@ def sync(
|
|
289
309
|
if not chunk_success:
|
290
310
|
return chunk_success, f"Unable to sync initial chunk for {p}:\n{chunk_msg}"
|
291
311
|
if debug:
|
292
|
-
dprint(
|
312
|
+
dprint("Successfully synced the first chunk, attemping the rest...")
|
293
313
|
|
294
314
|
failed_chunks = []
|
295
315
|
def _process_chunk(_chunk):
|
@@ -309,7 +329,6 @@ def sync(
|
|
309
329
|
)
|
310
330
|
)
|
311
331
|
|
312
|
-
|
313
332
|
results = sorted(
|
314
333
|
[(chunk_success, chunk_msg)] + (
|
315
334
|
list(pool.imap(_process_chunk, df))
|
@@ -329,7 +348,7 @@ def sync(
|
|
329
348
|
retry_success = True
|
330
349
|
if not success and any(success_bools):
|
331
350
|
if debug:
|
332
|
-
dprint(
|
351
|
+
dprint("Retrying failed chunks...")
|
333
352
|
chunks_to_retry = [c for c in failed_chunks]
|
334
353
|
failed_chunks = []
|
335
354
|
for chunk in chunks_to_retry:
|
@@ -361,9 +380,9 @@ def sync(
|
|
361
380
|
while run:
|
362
381
|
with Venv(get_connector_plugin(self.instance_connector)):
|
363
382
|
return_tuple = p.instance_connector.sync_pipe(
|
364
|
-
pipe
|
365
|
-
df
|
366
|
-
debug
|
383
|
+
pipe=p,
|
384
|
+
df=df,
|
385
|
+
debug=debug,
|
367
386
|
**kw
|
368
387
|
)
|
369
388
|
_retries += 1
|
@@ -382,7 +401,7 @@ def sync(
|
|
382
401
|
_checkpoint(**kw)
|
383
402
|
if self.cache_pipe is not None:
|
384
403
|
if debug:
|
385
|
-
dprint(
|
404
|
+
dprint("Caching retrieved dataframe.", **kw)
|
386
405
|
_sync_cache_tuple = self.cache_pipe.sync(df, debug=debug, **kw)
|
387
406
|
if not _sync_cache_tuple[0]:
|
388
407
|
warn(f"Failed to sync local cache for {self}.")
|
@@ -395,10 +414,10 @@ def sync(
|
|
395
414
|
return _sync(self, df = df)
|
396
415
|
|
397
416
|
from meerschaum.utils.threading import Thread
|
398
|
-
def default_callback(result_tuple
|
417
|
+
def default_callback(result_tuple: SuccessTuple):
|
399
418
|
dprint(f"Asynchronous result from {self}: {result_tuple}", **kw)
|
400
419
|
|
401
|
-
def default_error_callback(x
|
420
|
+
def default_error_callback(x: Exception):
|
402
421
|
dprint(f"Error received for {self}: {x}", **kw)
|
403
422
|
|
404
423
|
if callback is None and debug:
|
@@ -407,12 +426,12 @@ def sync(
|
|
407
426
|
error_callback = default_error_callback
|
408
427
|
try:
|
409
428
|
thread = Thread(
|
410
|
-
target
|
411
|
-
args
|
412
|
-
kwargs
|
413
|
-
daemon
|
414
|
-
callback
|
415
|
-
error_callback
|
429
|
+
target=_sync,
|
430
|
+
args=(self,),
|
431
|
+
kwargs={'df': df},
|
432
|
+
daemon=False,
|
433
|
+
callback=callback,
|
434
|
+
error_callback=error_callback,
|
416
435
|
)
|
417
436
|
thread.start()
|
418
437
|
except Exception as e:
|
@@ -424,12 +443,13 @@ def sync(
|
|
424
443
|
|
425
444
|
|
426
445
|
def get_sync_time(
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
446
|
+
self,
|
447
|
+
params: Optional[Dict[str, Any]] = None,
|
448
|
+
newest: bool = True,
|
449
|
+
apply_backtrack_interval: bool = False,
|
450
|
+
round_down: bool = False,
|
451
|
+
debug: bool = False
|
452
|
+
) -> Union['datetime', None]:
|
433
453
|
"""
|
434
454
|
Get the most recent datetime value for a Pipe.
|
435
455
|
|
@@ -443,6 +463,9 @@ def get_sync_time(
|
|
443
463
|
If `True`, get the most recent datetime (honoring `params`).
|
444
464
|
If `False`, get the oldest datetime (`ASC` instead of `DESC`).
|
445
465
|
|
466
|
+
apply_backtrack_interval: bool, default False
|
467
|
+
If `True`, subtract the backtrack interval from the sync time.
|
468
|
+
|
446
469
|
round_down: bool, default False
|
447
470
|
If `True`, round down the datetime value to the nearest minute.
|
448
471
|
|
@@ -461,15 +484,22 @@ def get_sync_time(
|
|
461
484
|
with Venv(get_connector_plugin(self.instance_connector)):
|
462
485
|
sync_time = self.instance_connector.get_sync_time(
|
463
486
|
self,
|
464
|
-
params
|
465
|
-
newest
|
466
|
-
debug
|
487
|
+
params=params,
|
488
|
+
newest=newest,
|
489
|
+
debug=debug,
|
467
490
|
)
|
468
491
|
|
469
|
-
if
|
470
|
-
|
492
|
+
if round_down and isinstance(sync_time, datetime):
|
493
|
+
sync_time = round_time(sync_time, timedelta(minutes=1))
|
494
|
+
|
495
|
+
if apply_backtrack_interval and sync_time is not None:
|
496
|
+
backtrack_interval = self.get_backtrack_interval(debug=debug)
|
497
|
+
try:
|
498
|
+
sync_time -= backtrack_interval
|
499
|
+
except Exception as e:
|
500
|
+
warn(f"Failed to apply backtrack interval:\n{e}")
|
471
501
|
|
472
|
-
return
|
502
|
+
return sync_time
|
473
503
|
|
474
504
|
|
475
505
|
def exists(
|
@@ -0,0 +1,333 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# vim:fenc=utf-8
|
4
|
+
|
5
|
+
"""
|
6
|
+
Define the bootstrapping wizard for creating plugins.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import re
|
10
|
+
import pathlib
|
11
|
+
import meerschaum as mrsm
|
12
|
+
from meerschaum.utils.typing import Any, SuccessTuple, Dict, List
|
13
|
+
from meerschaum.utils.warnings import warn, info
|
14
|
+
from meerschaum.utils.prompt import prompt, choose, yes_no
|
15
|
+
from meerschaum.utils.formatting._shell import clear_screen
|
16
|
+
|
17
|
+
FEATURE_CHOICES: Dict[str, str] = {
|
18
|
+
'fetch' : 'Fetch data\n (e.g. extracting from a remote API)\n',
|
19
|
+
'connector': 'Custom connector\n (e.g. manage credentials)\n',
|
20
|
+
'action' : 'New actions\n (e.g. `mrsm sing song`)\n',
|
21
|
+
'api' : 'New API endpoints\n (e.g. `POST /my/new/endpoint`)\n',
|
22
|
+
'web' : 'New web console page\n (e.g. `/dash/my-web-app`)\n',
|
23
|
+
}
|
24
|
+
|
25
|
+
IMPORTS_LINES: Dict[str, str] = {
|
26
|
+
'stdlib': (
|
27
|
+
"from datetime import datetime, timedelta, timezone\n"
|
28
|
+
),
|
29
|
+
'default': (
|
30
|
+
"import meerschaum as mrsm\n"
|
31
|
+
"from meerschaum.config import get_plugin_config, write_plugin_config\n"
|
32
|
+
),
|
33
|
+
'connector': (
|
34
|
+
"from meerschaum.connectors import Connector, make_connector\n"
|
35
|
+
),
|
36
|
+
'action': (
|
37
|
+
"from meerschaum.actions import make_action\n"
|
38
|
+
),
|
39
|
+
'api': (
|
40
|
+
"from meerschaum.plugins import api_plugin\n"
|
41
|
+
),
|
42
|
+
'web': (
|
43
|
+
"from meerschaum.plugins import web_page, dash_plugin\n"
|
44
|
+
),
|
45
|
+
'api+web': (
|
46
|
+
"from meerschaum.plugins import api_plugin, web_page, dash_plugin\n"
|
47
|
+
),
|
48
|
+
}
|
49
|
+
|
50
|
+
### TODO: Add feature for custom connectors.
|
51
|
+
FEATURE_LINES: Dict[str, str] = {
|
52
|
+
'header': (
|
53
|
+
"#! /usr/bin/env python3\n"
|
54
|
+
"# -*- coding: utf-8 -*-\n\n"
|
55
|
+
"\"\"\"\n"
|
56
|
+
"Implement the plugin '{plugin_name}'.\n\n"
|
57
|
+
"See the Writing Plugins guide for more information:\n"
|
58
|
+
"https://meerschaum.io/reference/plugins/writing-plugins/\n"
|
59
|
+
"\"\"\"\n\n"
|
60
|
+
),
|
61
|
+
'default': (
|
62
|
+
"__version__ = '0.0.1'\n"
|
63
|
+
"\n# Add any dependencies to `required` (similar to `requirements.txt`).\n"
|
64
|
+
"required: list[str] = []\n\n\n"
|
65
|
+
),
|
66
|
+
'setup': (
|
67
|
+
"def setup(**kwargs) -> mrsm.SuccessTuple:\n"
|
68
|
+
" \"\"\"Executed during installation and `mrsm setup plugin {plugin_name}`.\"\"\"\n"
|
69
|
+
" return True, \"Success\"\n\n\n"
|
70
|
+
),
|
71
|
+
'register': (
|
72
|
+
"def register(pipe: mrsm.Pipe):\n"
|
73
|
+
" \"\"\"Return the default parameters for a new pipe.\"\"\"\n"
|
74
|
+
" return {{\n"
|
75
|
+
" 'columns': {{\n"
|
76
|
+
" 'datetime': {dt_col_name},\n"
|
77
|
+
" }}\n"
|
78
|
+
" }}\n\n\n"
|
79
|
+
),
|
80
|
+
'fetch': (
|
81
|
+
"def fetch(\n"
|
82
|
+
" pipe: mrsm.Pipe,\n"
|
83
|
+
" begin: datetime | None = None,\n"
|
84
|
+
" end: datetime | None = None,\n"
|
85
|
+
" **kwargs\n"
|
86
|
+
"):\n"
|
87
|
+
" \"\"\"Return or yield dataframes.\"\"\"\n"
|
88
|
+
" docs = []\n"
|
89
|
+
" # populate docs with dictionaries (rows).\n"
|
90
|
+
" return docs\n\n\n"
|
91
|
+
),
|
92
|
+
'connector': (
|
93
|
+
"@make_connector\n"
|
94
|
+
"class {plugin_name_capitalized}Connector(Connector):\n"
|
95
|
+
" \"\"\"Implement '{plugin_name_lower}' connectors.\"\"\"\n\n"
|
96
|
+
" REQUIRED_ATTRIBUTES: list[str] = []\n"
|
97
|
+
" \n"
|
98
|
+
" def fetch(\n"
|
99
|
+
" self,\n"
|
100
|
+
" pipe: mrsm.Pipe,\n"
|
101
|
+
" begin: datetime | None = None,\n"
|
102
|
+
" end: datetime | None = None,\n"
|
103
|
+
" **kwargs\n"
|
104
|
+
" ):\n"
|
105
|
+
" \"\"\"Return or yield dataframes.\"\"\"\n"
|
106
|
+
" docs = []\n"
|
107
|
+
" # populate docs with dictionaries (rows).\n"
|
108
|
+
" return docs\n\n\n"
|
109
|
+
),
|
110
|
+
'action': (
|
111
|
+
"@make_action\n"
|
112
|
+
"def {action_name}(**kwargs) -> mrsm.SuccessTuple:\n"
|
113
|
+
" \"\"\"Run `mrsm {action_spaces}` to trigger.\"\"\"\n"
|
114
|
+
" return True, \"Success\"\n\n\n"
|
115
|
+
),
|
116
|
+
'api': (
|
117
|
+
"@api_plugin\n"
|
118
|
+
"def init_app(fastapi_app):\n"
|
119
|
+
" \"\"\"Add new endpoints to the FastAPI app.\"\"\"\n\n"
|
120
|
+
" import fastapi\n"
|
121
|
+
" from meerschaum.api import manager\n\n"
|
122
|
+
" @fastapi_app.get('/{plugin_name}')\n"
|
123
|
+
" def get_my_endpoint(curr_user=fastapi.Depends(manager)):\n"
|
124
|
+
" return {{'message': \"Hello from plugin '{plugin_name}'!\"}}\n\n\n"
|
125
|
+
),
|
126
|
+
'web': (
|
127
|
+
"@dash_plugin\n"
|
128
|
+
"def init_dash(dash_app):\n"
|
129
|
+
" \"\"\"Initialize the Plotly Dash application.\"\"\"\n\n"
|
130
|
+
" import dash.html as html\n"
|
131
|
+
" import dash.dcc as dcc\n"
|
132
|
+
" from dash import Input, Output, State, no_update\n"
|
133
|
+
" import dash_bootstrap_components as dbc\n\n"
|
134
|
+
" # Create a new page at the path `/dash/{plugin_name}`.\n"
|
135
|
+
" @web_page('{plugin_name}', login_required=False)\n"
|
136
|
+
" def page_layout():\n"
|
137
|
+
" \"\"\"Return the layout objects for this page.\"\"\"\n"
|
138
|
+
" return dbc.Container([\n"
|
139
|
+
" dcc.Location(id='{plugin_name}-location'),\n"
|
140
|
+
" html.Div(id='output-div'),\n"
|
141
|
+
" ])\n\n"
|
142
|
+
" @dash_app.callback(\n"
|
143
|
+
" Output('output-div', 'children'),\n"
|
144
|
+
" Input('{plugin_name}-location', 'pathname'),\n"
|
145
|
+
" )\n"
|
146
|
+
" def render_page_on_url_change(pathname: str):\n"
|
147
|
+
" \"\"\"Reload page contents when the URL path changes.\"\"\"\n"
|
148
|
+
" return html.H1(\"Hello from plugin '{plugin_name}'!\")\n\n\n"
|
149
|
+
),
|
150
|
+
}
|
151
|
+
|
152
|
+
def bootstrap_plugin(
|
153
|
+
plugin_name: str,
|
154
|
+
debug: bool = False,
|
155
|
+
**kwargs: Any
|
156
|
+
) -> SuccessTuple:
|
157
|
+
"""
|
158
|
+
Prompt the user for features and create a plugin file.
|
159
|
+
"""
|
160
|
+
from meerschaum.utils.misc import edit_file
|
161
|
+
plugins_dir_path = _get_plugins_dir_path()
|
162
|
+
clear_screen(debug=debug)
|
163
|
+
info(
|
164
|
+
"Answer the questions below to pick out features.\n"
|
165
|
+
+ " See the Writing Plugins guide for documentation:\n"
|
166
|
+
+ " https://meerschaum.io/reference/plugins/writing-plugins/ "
|
167
|
+
+ "for documentation.\n"
|
168
|
+
)
|
169
|
+
|
170
|
+
plugin = mrsm.Plugin(plugin_name)
|
171
|
+
if plugin.is_installed():
|
172
|
+
uninstall_success, uninstall_msg = _ask_to_uninstall(plugin)
|
173
|
+
if not uninstall_success:
|
174
|
+
return uninstall_success, uninstall_msg
|
175
|
+
clear_screen(debug=debug)
|
176
|
+
|
177
|
+
features: List[str] = choose(
|
178
|
+
"Which of the following features would you like to add to your plugin?",
|
179
|
+
list(FEATURE_CHOICES.items()),
|
180
|
+
default = 'fetch',
|
181
|
+
multiple = True,
|
182
|
+
as_indices = True,
|
183
|
+
**kwargs
|
184
|
+
)
|
185
|
+
|
186
|
+
clear_screen(debug=debug)
|
187
|
+
|
188
|
+
action_name = ''
|
189
|
+
if 'action' in features:
|
190
|
+
action_name = _get_action_name()
|
191
|
+
clear_screen(debug=debug)
|
192
|
+
action_spaces = action_name.replace('_', ' ')
|
193
|
+
|
194
|
+
dt_col_name = None
|
195
|
+
if 'fetch' in features:
|
196
|
+
dt_col_name = _get_quoted_dt_col_name()
|
197
|
+
clear_screen(debug=debug)
|
198
|
+
|
199
|
+
plugin_labels = {
|
200
|
+
'plugin_name': plugin_name,
|
201
|
+
'plugin_name_capitalized': re.split(
|
202
|
+
r'[-_]', plugin_name.lower()
|
203
|
+
)[0].capitalize(),
|
204
|
+
'plugin_name_lower': plugin_name.lower(),
|
205
|
+
'action_name': action_name,
|
206
|
+
'action_spaces': action_spaces,
|
207
|
+
'dt_col_name': dt_col_name,
|
208
|
+
}
|
209
|
+
|
210
|
+
body_text = ""
|
211
|
+
body_text += FEATURE_LINES['header'].format(**plugin_labels)
|
212
|
+
body_text += IMPORTS_LINES['stdlib'].format(**plugin_labels)
|
213
|
+
body_text += IMPORTS_LINES['default'].format(**plugin_labels)
|
214
|
+
if 'connector' in features:
|
215
|
+
body_text += IMPORTS_LINES['connector'].format(**plugin_labels)
|
216
|
+
if 'action' in features:
|
217
|
+
body_text += IMPORTS_LINES['action'].format(**plugin_labels)
|
218
|
+
if 'api' in features and 'web' in features:
|
219
|
+
body_text += IMPORTS_LINES['api+web'].format(**plugin_labels)
|
220
|
+
elif 'api' in features:
|
221
|
+
body_text += IMPORTS_LINES['api'].format(**plugin_labels)
|
222
|
+
elif 'web' in features:
|
223
|
+
body_text += IMPORTS_LINES['web'].format(**plugin_labels)
|
224
|
+
|
225
|
+
body_text += "\n"
|
226
|
+
body_text += FEATURE_LINES['default'].format(**plugin_labels)
|
227
|
+
body_text += FEATURE_LINES['setup'].format(**plugin_labels)
|
228
|
+
|
229
|
+
if 'fetch' in features:
|
230
|
+
body_text += FEATURE_LINES['register'].format(**plugin_labels)
|
231
|
+
body_text += FEATURE_LINES['fetch'].format(**plugin_labels)
|
232
|
+
|
233
|
+
if 'connector' in features:
|
234
|
+
body_text += FEATURE_LINES['connector'].format(**plugin_labels)
|
235
|
+
|
236
|
+
if 'action' in features:
|
237
|
+
body_text += FEATURE_LINES['action'].format(**plugin_labels)
|
238
|
+
|
239
|
+
if 'api' in features:
|
240
|
+
body_text += FEATURE_LINES['api'].format(**plugin_labels)
|
241
|
+
|
242
|
+
if 'web' in features:
|
243
|
+
body_text += FEATURE_LINES['web'].format(**plugin_labels)
|
244
|
+
|
245
|
+
try:
|
246
|
+
plugin_path = plugins_dir_path / (plugin_name + '.py')
|
247
|
+
with open(plugin_path, 'w+', encoding='utf-8') as f:
|
248
|
+
f.write(body_text.rstrip())
|
249
|
+
except Exception as e:
|
250
|
+
error_msg = f"Failed to write file '{plugin_path}':\n{e}"
|
251
|
+
return False, error_msg
|
252
|
+
|
253
|
+
clear_screen(debug=debug)
|
254
|
+
mrsm.pprint((True, f"Successfully created file '{plugin_path}'."))
|
255
|
+
try:
|
256
|
+
_ = prompt(
|
257
|
+
f"Press [Enter] to edit plugin '{plugin_name}',"
|
258
|
+
+ " [CTRL+C] to skip.",
|
259
|
+
icon = False,
|
260
|
+
)
|
261
|
+
except (KeyboardInterrupt, Exception):
|
262
|
+
return True, "Success"
|
263
|
+
|
264
|
+
edit_file(plugin_path, debug=debug)
|
265
|
+
return True, "Success"
|
266
|
+
|
267
|
+
|
268
|
+
def _get_plugins_dir_path() -> pathlib.Path:
|
269
|
+
from meerschaum.config.paths import PLUGINS_DIR_PATHS
|
270
|
+
|
271
|
+
if not PLUGINS_DIR_PATHS:
|
272
|
+
raise EnvironmentError("No plugin dir path could be found.")
|
273
|
+
|
274
|
+
if len(PLUGINS_DIR_PATHS) == 1:
|
275
|
+
return PLUGINS_DIR_PATHS[0]
|
276
|
+
|
277
|
+
return pathlib.Path(
|
278
|
+
choose(
|
279
|
+
"In which directory do you want to write your plugin?",
|
280
|
+
[path.as_posix() for path in PLUGINS_DIR_PATHS],
|
281
|
+
numeric = True,
|
282
|
+
multiple = False,
|
283
|
+
default = PLUGINS_DIR_PATHS[0].as_posix(),
|
284
|
+
)
|
285
|
+
)
|
286
|
+
|
287
|
+
|
288
|
+
def _ask_to_uninstall(plugin: mrsm.Plugin, **kwargs: Any) -> SuccessTuple:
|
289
|
+
from meerschaum._internal.entry import entry
|
290
|
+
warn(f"Plugin '{plugin}' is already installed!", stack=False)
|
291
|
+
uninstall_plugin = yes_no(
|
292
|
+
f"Do you want to first uninstall '{plugin}'?",
|
293
|
+
default = 'n',
|
294
|
+
**kwargs
|
295
|
+
)
|
296
|
+
if not uninstall_plugin:
|
297
|
+
return False, f"Plugin '{plugin}' already exists."
|
298
|
+
|
299
|
+
return entry(['uninstall', 'plugin', plugin.name, '-f'])
|
300
|
+
|
301
|
+
|
302
|
+
def _get_action_name() -> str:
|
303
|
+
while True:
|
304
|
+
try:
|
305
|
+
action_name = prompt(
|
306
|
+
"What is name of your action?\n "
|
307
|
+
+ "(separate subactions with spaces, e.g. `sing song`):"
|
308
|
+
).replace(' ', '_')
|
309
|
+
except KeyboardInterrupt:
|
310
|
+
return False, "Aborted plugin creation."
|
311
|
+
|
312
|
+
if action_name:
|
313
|
+
break
|
314
|
+
warn("Please enter an action.", stack=False)
|
315
|
+
return action_name
|
316
|
+
|
317
|
+
|
318
|
+
def _get_quoted_dt_col_name() -> str:
|
319
|
+
try:
|
320
|
+
dt_col_name = prompt(
|
321
|
+
"Enter the datetime column name ([CTRL+C] to skip):"
|
322
|
+
)
|
323
|
+
except (Exception, KeyboardInterrupt):
|
324
|
+
dt_col_name = None
|
325
|
+
|
326
|
+
if dt_col_name is None:
|
327
|
+
dt_col_name = 'None'
|
328
|
+
elif '"' in dt_col_name or "'" in dt_col_name:
|
329
|
+
dt_col_name = f"\"\"\"{dt_col_name}\"\"\""
|
330
|
+
else:
|
331
|
+
dt_col_name = f"\"{dt_col_name}\""
|
332
|
+
|
333
|
+
return dt_col_name
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
10
10
|
import platform
|
11
11
|
import os
|
12
12
|
import sys
|
13
|
-
from meerschaum.utils.typing import Optional, Union, Any
|
13
|
+
from meerschaum.utils.typing import Optional, Union, Any, Dict
|
14
14
|
from meerschaum.utils.formatting._shell import make_header
|
15
15
|
from meerschaum.utils.formatting._pprint import pprint
|
16
16
|
from meerschaum.utils.formatting._pipes import (
|
@@ -298,6 +298,7 @@ def print_options(
|
|
298
298
|
header: Optional[str] = None,
|
299
299
|
num_cols: Optional[int] = None,
|
300
300
|
adjust_cols: bool = True,
|
301
|
+
sort_options: bool = False,
|
301
302
|
**kw
|
302
303
|
) -> None:
|
303
304
|
"""
|
@@ -339,6 +340,8 @@ def print_options(
|
|
339
340
|
_options = []
|
340
341
|
for o in options:
|
341
342
|
_options.append(str(o))
|
343
|
+
if sort_options:
|
344
|
+
_options = sorted(_options)
|
342
345
|
_header = f"Available {name}" if header is None else header
|
343
346
|
|
344
347
|
if num_cols is None:
|
@@ -349,7 +352,7 @@ def print_options(
|
|
349
352
|
print()
|
350
353
|
print(make_header(_header))
|
351
354
|
### print actions
|
352
|
-
for option in
|
355
|
+
for option in _options:
|
353
356
|
if not nopretty:
|
354
357
|
print(" - ", end="")
|
355
358
|
print(option)
|
@@ -385,7 +388,7 @@ def print_options(
|
|
385
388
|
|
386
389
|
if _header is not None:
|
387
390
|
table = Table(
|
388
|
-
title = '\n' + _header,
|
391
|
+
title = ('\n' + _header) if header else header,
|
389
392
|
box = box.SIMPLE,
|
390
393
|
show_header = False,
|
391
394
|
show_footer = False,
|
@@ -397,13 +400,22 @@ def print_options(
|
|
397
400
|
for i in range(num_cols):
|
398
401
|
table.add_column()
|
399
402
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
403
|
+
if len(_options) < 12:
|
404
|
+
# If fewer than 12 items, use a single column
|
405
|
+
for option in _options:
|
406
|
+
table.add_row(Text.from_ansi(highlight_pipes(option)))
|
407
|
+
else:
|
408
|
+
# Otherwise, use multiple columns as before
|
409
|
+
num_rows = (len(_options) + num_cols - 1) // num_cols
|
410
|
+
for i in range(num_rows):
|
411
|
+
row = []
|
412
|
+
for j in range(num_cols):
|
413
|
+
index = i + j * num_rows
|
414
|
+
if index < len(_options):
|
415
|
+
row.append(Text.from_ansi(highlight_pipes(_options[index])))
|
416
|
+
else:
|
417
|
+
row.append('')
|
418
|
+
table.add_row(*row)
|
407
419
|
|
408
420
|
get_console().print(table)
|
409
421
|
return None
|