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.
Files changed (30) hide show
  1. meerschaum/_internal/arguments/_parse_arguments.py +23 -14
  2. meerschaum/_internal/arguments/_parser.py +4 -2
  3. meerschaum/_internal/docs/index.py +31 -125
  4. meerschaum/_internal/shell/Shell.py +0 -3
  5. meerschaum/actions/bootstrap.py +14 -235
  6. meerschaum/actions/edit.py +98 -15
  7. meerschaum/actions/show.py +13 -4
  8. meerschaum/actions/stack.py +12 -12
  9. meerschaum/actions/uninstall.py +24 -29
  10. meerschaum/api/__init__.py +0 -1
  11. meerschaum/api/dash/__init__.py +0 -1
  12. meerschaum/api/dash/callbacks/custom.py +1 -1
  13. meerschaum/api/dash/plugins.py +5 -6
  14. meerschaum/config/__init__.py +16 -6
  15. meerschaum/config/_version.py +1 -1
  16. meerschaum/core/Pipe/_fetch.py +25 -21
  17. meerschaum/core/Pipe/_sync.py +89 -59
  18. meerschaum/plugins/bootstrap.py +333 -0
  19. meerschaum/utils/formatting/__init__.py +22 -10
  20. meerschaum/utils/prompt.py +11 -4
  21. meerschaum/utils/warnings.py +1 -0
  22. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/METADATA +1 -1
  23. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/RECORD +29 -29
  24. meerschaum/actions/backup.py +0 -43
  25. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/LICENSE +0 -0
  26. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/NOTICE +0 -0
  27. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/WHEEL +0 -0
  28. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/entry_points.txt +0 -0
  29. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/top_level.txt +0 -0
  30. {meerschaum-2.2.5.dev0.dist-info → meerschaum-2.2.5.dev3.dist-info}/zip-safe +0 -0
@@ -34,29 +34,29 @@ class InferFetch:
34
34
  MRSM_INFER_FETCH: bool = True
35
35
 
36
36
  def sync(
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,
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:
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 as e:
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(p, debug=debug, **kw)
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(p, debug=debug, **kw)
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(debug=debug, **kw)
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(f"Successfully synced the first chunk, attemping the rest...")
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(f"Retrying failed chunks...")
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 = p,
365
- df = df,
366
- debug = 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(f"Caching retrieved dataframe.", **kw)
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 : SuccessTuple):
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 : Exception):
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 = _sync,
411
- args = (self,),
412
- kwargs = {'df' : df},
413
- daemon = False,
414
- callback = callback,
415
- error_callback = 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
- self,
428
- params: Optional[Dict[str, Any]] = None,
429
- newest: bool = True,
430
- round_down: bool = False,
431
- debug: bool = False
432
- ) -> Union['datetime', None]:
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 = params,
465
- newest = newest,
466
- debug = debug,
487
+ params=params,
488
+ newest=newest,
489
+ debug=debug,
467
490
  )
468
491
 
469
- if not round_down or not isinstance(sync_time, datetime):
470
- return sync_time
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 round_time(sync_time, timedelta(minutes=1))
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 sorted(_options):
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
- chunks = iterate_chunks(
401
- [Text.from_ansi(highlight_pipes(o)) for o in sorted(_options)],
402
- num_cols,
403
- fillvalue=''
404
- )
405
- for c in chunks:
406
- table.add_row(*c)
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