meerschaum 2.2.5.dev3__py3-none-any.whl → 2.2.7__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 (65) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/__main__.py +10 -5
  3. meerschaum/_internal/arguments/_parser.py +13 -2
  4. meerschaum/_internal/docs/index.py +523 -26
  5. meerschaum/_internal/entry.py +13 -13
  6. meerschaum/_internal/shell/Shell.py +26 -22
  7. meerschaum/_internal/shell/updates.py +175 -0
  8. meerschaum/_internal/term/__init__.py +2 -2
  9. meerschaum/actions/bootstrap.py +13 -14
  10. meerschaum/actions/python.py +11 -8
  11. meerschaum/actions/register.py +149 -37
  12. meerschaum/actions/show.py +79 -71
  13. meerschaum/actions/stop.py +11 -11
  14. meerschaum/actions/sync.py +3 -3
  15. meerschaum/actions/upgrade.py +28 -36
  16. meerschaum/api/dash/callbacks/login.py +21 -13
  17. meerschaum/api/dash/pages/login.py +2 -2
  18. meerschaum/api/routes/_login.py +5 -5
  19. meerschaum/api/routes/_pipes.py +20 -20
  20. meerschaum/config/__init__.py +8 -1
  21. meerschaum/config/_formatting.py +1 -0
  22. meerschaum/config/_paths.py +24 -2
  23. meerschaum/config/_shell.py +78 -66
  24. meerschaum/config/_version.py +1 -1
  25. meerschaum/config/paths.py +21 -2
  26. meerschaum/config/static/__init__.py +2 -0
  27. meerschaum/connectors/Connector.py +7 -2
  28. meerschaum/connectors/__init__.py +7 -5
  29. meerschaum/connectors/api/APIConnector.py +7 -2
  30. meerschaum/connectors/api/_actions.py +23 -31
  31. meerschaum/connectors/api/_misc.py +1 -1
  32. meerschaum/connectors/api/_request.py +13 -9
  33. meerschaum/connectors/api/_uri.py +5 -5
  34. meerschaum/core/Pipe/__init__.py +7 -3
  35. meerschaum/core/Pipe/_data.py +23 -15
  36. meerschaum/core/Pipe/_deduplicate.py +1 -1
  37. meerschaum/core/Pipe/_dtypes.py +5 -0
  38. meerschaum/core/Pipe/_fetch.py +18 -16
  39. meerschaum/core/Pipe/_sync.py +23 -15
  40. meerschaum/plugins/_Plugin.py +6 -6
  41. meerschaum/plugins/__init__.py +1 -1
  42. meerschaum/utils/daemon/Daemon.py +88 -129
  43. meerschaum/utils/daemon/FileDescriptorInterceptor.py +14 -5
  44. meerschaum/utils/daemon/RotatingFile.py +23 -17
  45. meerschaum/utils/daemon/__init__.py +28 -21
  46. meerschaum/utils/dataframe.py +12 -4
  47. meerschaum/utils/debug.py +9 -15
  48. meerschaum/utils/formatting/__init__.py +92 -46
  49. meerschaum/utils/formatting/_jobs.py +47 -9
  50. meerschaum/utils/misc.py +117 -11
  51. meerschaum/utils/packages/__init__.py +28 -16
  52. meerschaum/utils/prompt.py +5 -0
  53. meerschaum/utils/schedule.py +21 -15
  54. meerschaum/utils/typing.py +1 -0
  55. meerschaum/utils/venv/__init__.py +5 -1
  56. meerschaum/utils/warnings.py +8 -1
  57. meerschaum/utils/yaml.py +2 -2
  58. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/METADATA +1 -1
  59. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/RECORD +65 -64
  60. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/WHEEL +1 -1
  61. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/LICENSE +0 -0
  62. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/NOTICE +0 -0
  63. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/entry_points.txt +0 -0
  64. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/top_level.txt +0 -0
  65. {meerschaum-2.2.5.dev3.dist-info → meerschaum-2.2.7.dist-info}/zip-safe +0 -0
@@ -8,20 +8,15 @@ The entry point for launching Meerschaum actions.
8
8
  """
9
9
 
10
10
  from __future__ import annotations
11
- from meerschaum.utils.typing import SuccessTuple, List, Optional, Dict
11
+ from meerschaum.utils.typing import SuccessTuple, List, Optional, Dict, Callable, Any
12
12
 
13
13
  def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
14
- """Parse arguments and launch a Meerschaum action.
15
- The `action` list removes the first element.
16
-
17
- Examples of action:
18
- 'show actions' -> ['actions']
19
- 'show' -> []
14
+ """
15
+ Parse arguments and launch a Meerschaum action.
20
16
 
21
17
  Returns
22
18
  -------
23
- A `SuccessTuple` indicating success. If `schedule` is provided, this will never return.
24
-
19
+ A `SuccessTuple` indicating success.
25
20
  """
26
21
  from meerschaum._internal.arguments import parse_arguments
27
22
  from meerschaum.config.static import STATIC_CONFIG
@@ -51,9 +46,9 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
51
46
 
52
47
 
53
48
  def entry_with_args(
54
- _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
55
- **kw
56
- ) -> SuccessTuple:
49
+ _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
50
+ **kw
51
+ ) -> SuccessTuple:
57
52
  """Execute a Meerschaum action with keyword arguments.
58
53
  Use `_entry()` for parsing sysargs before executing.
59
54
  """
@@ -96,7 +91,12 @@ def entry_with_args(
96
91
 
97
92
  kw['action'] = remove_leading_action(kw['action'], _actions=_actions)
98
93
 
99
- do_action = functools.partial(_do_action_wrapper, action_function, plugin_name, **kw)
94
+ do_action = functools.partial(
95
+ _do_action_wrapper,
96
+ action_function,
97
+ plugin_name,
98
+ **kw
99
+ )
100
100
  if kw.get('schedule', None) and not skip_schedule:
101
101
  from meerschaum.utils.schedule import schedule_function
102
102
  from meerschaum.utils.misc import interval_str
@@ -95,8 +95,8 @@ def _insert_shell_actions(
95
95
  setattr(_shell_class, 'complete_' + a, completer)
96
96
 
97
97
  def _completer_wrapper(
98
- target: Callable[[Any], List[str]]
99
- ) -> Callable[['meerschaum._internal.shell.Shell', str, str, int, int], Any]:
98
+ target: Callable[[Any], List[str]]
99
+ ) -> Callable[['meerschaum._internal.shell.Shell', str, str, int, int], Any]:
100
100
  """
101
101
  Wrapper for `complete_` functions so they can instead use Meerschaum arguments.
102
102
  """
@@ -125,13 +125,13 @@ def _completer_wrapper(
125
125
 
126
126
 
127
127
  def default_action_completer(
128
- text: Optional[str] = None,
129
- line: Optional[str] = None,
130
- begin_index: Optional[int] = None,
131
- end_index: Optional[int] = None,
132
- action: Optional[List[str]] = None,
133
- **kw: Any
134
- ) -> List[str]:
128
+ text: Optional[str] = None,
129
+ line: Optional[str] = None,
130
+ begin_index: Optional[int] = None,
131
+ end_index: Optional[int] = None,
132
+ action: Optional[List[str]] = None,
133
+ **kw: Any
134
+ ) -> List[str]:
135
135
  """
136
136
  Search for subactions by default. This may be overridden by each action.
137
137
  """
@@ -206,13 +206,11 @@ def get_shell_intro(with_color: bool = True) -> str:
206
206
  """
207
207
  from meerschaum.utils.formatting import CHARSET, ANSI, colored
208
208
  intro = get_config('shell', CHARSET, 'intro', patch=patch)
209
- intro += '\n' + ''.join(
210
- [' '
211
- for i in range(
212
- string_width(intro) - len('v' + version)
213
- )
214
- ]
215
- ) + 'v' + version
209
+ intro += (
210
+ '\n'
211
+ + (' ' * (string_width(intro) - len('v' + version)))
212
+ + f'v{version}'
213
+ )
216
214
 
217
215
  if not with_color or not ANSI:
218
216
  return intro
@@ -225,10 +223,10 @@ def get_shell_intro(with_color: bool = True) -> str:
225
223
 
226
224
  class Shell(cmd.Cmd):
227
225
  def __init__(
228
- self,
229
- actions: Optional[Dict[str, Any]] = None,
230
- sysargs: Optional[List[str]] = None
231
- ):
226
+ self,
227
+ actions: Optional[Dict[str, Any]] = None,
228
+ sysargs: Optional[List[str]] = None
229
+ ):
232
230
  """
233
231
  Customize the CLI from configuration
234
232
  """
@@ -297,19 +295,25 @@ class Shell(cmd.Cmd):
297
295
  except Exception as e:
298
296
  pass
299
297
 
298
+ ### Finally, spawn the version update thread.
299
+ from meerschaum._internal.shell.updates import run_version_check_thread
300
+ self._update_thread = run_version_check_thread(debug=shell_attrs.get('debug', False))
301
+
302
+
300
303
  def load_config(self, instance: Optional[str] = None):
301
304
  """
302
305
  Set attributes from the shell configuration.
303
306
  """
304
307
  from meerschaum.utils.misc import remove_ansi
305
308
  from meerschaum.utils.formatting import CHARSET, ANSI, colored
306
-
309
+ from meerschaum._internal.shell.updates import get_update_message
310
+
307
311
  if shell_attrs.get('intro', None) != '':
308
312
  self.intro = (
309
313
  get_shell_intro(with_color=False)
310
314
  if shell_attrs.get('intro', None) != ''
311
315
  else ""
312
- )
316
+ ) + get_update_message()
313
317
 
314
318
  shell_attrs['intro'] = self.intro
315
319
  shell_attrs['_prompt'] = get_config('shell', CHARSET, 'prompt', patch=patch)
@@ -0,0 +1,175 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ If configured, check `api:mrsm` for announcement messages.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime, timezone, timedelta
11
+
12
+ import meerschaum as mrsm
13
+ from meerschaum.utils.typing import Union, SuccessTuple, Optional
14
+ from meerschaum.config import get_config
15
+ from meerschaum.utils.formatting import CHARSET, ANSI, colored
16
+ from meerschaum.utils.misc import string_width, remove_ansi
17
+ from meerschaum.config.paths import (
18
+ UPDATES_LOCK_PATH,
19
+ UPDATES_CACHE_PATH,
20
+ )
21
+ from meerschaum.utils.threading import Thread
22
+
23
+
24
+ def cache_remote_version(debug: bool = False) -> SuccessTuple:
25
+ """
26
+ Fetch and cache the latest version if available.
27
+ """
28
+ allow_update_check = get_config('shell', 'updates', 'check_remote')
29
+ if not allow_update_check:
30
+ return True, "Update checks are disabled."
31
+
32
+ refresh_minutes = get_config('shell', 'updates', 'refresh_minutes')
33
+ update_delta = timedelta(minutes=refresh_minutes)
34
+
35
+ if UPDATES_CACHE_PATH.exists():
36
+ try:
37
+ with open(UPDATES_CACHE_PATH, 'r', encoding='utf8') as f:
38
+ cache_dict = json.load(f)
39
+ except Exception:
40
+ cache_dict = {}
41
+ else:
42
+ cache_dict = {}
43
+
44
+ now = datetime.now(timezone.utc)
45
+ last_check_ts_str = cache_dict.get('last_check_ts')
46
+ last_check_ts = datetime.fromisoformat(last_check_ts_str) if last_check_ts_str else None
47
+
48
+ need_update = (
49
+ last_check_ts_str is None
50
+ or ((now - last_check_ts) >= update_delta)
51
+ )
52
+
53
+ if not need_update:
54
+ return True, "No updates are needed."
55
+
56
+ try:
57
+ conn = mrsm.get_connector('api:mrsm')
58
+ remote_version = conn.get_mrsm_version(debug=debug, timeout=3)
59
+ except Exception:
60
+ remote_version = None
61
+
62
+ if remote_version is None:
63
+ return False, "Could not determine remote version."
64
+
65
+ with open(UPDATES_CACHE_PATH, 'w+', encoding='utf-8') as f:
66
+ json.dump(
67
+ {
68
+ 'last_check_ts': now.isoformat(),
69
+ 'remote_version': remote_version,
70
+ },
71
+ f,
72
+ )
73
+
74
+ return True, "Updated remote version cache."
75
+
76
+
77
+ def run_version_check_thread(debug: bool = False) -> Union[Thread, None]:
78
+ """
79
+ Run the version update check in a separate thread.
80
+ """
81
+ allow_update_check = get_config('shell', 'updates', 'check_remote')
82
+ if not allow_update_check:
83
+ return None
84
+
85
+ thread = Thread(
86
+ target=cache_remote_version,
87
+ daemon=True,
88
+ kwargs={'debug': debug},
89
+ )
90
+ thread.start()
91
+ return thread
92
+
93
+
94
+ _remote_version: Optional[str] = None
95
+ def get_remote_version_from_cache() -> Optional[str]:
96
+ """
97
+ Return the version string from the local cache file.
98
+ """
99
+ global _remote_version
100
+ try:
101
+ with open(UPDATES_CACHE_PATH, 'r', encoding='utf-8') as f:
102
+ cache_dict = json.load(f)
103
+ except Exception:
104
+ return None
105
+
106
+ _remote_version = cache_dict.get('remote_version')
107
+ return _remote_version
108
+
109
+
110
+ _out_of_date: Optional[bool] = None
111
+ def mrsm_out_of_date() -> bool:
112
+ """
113
+ Determine whether to print the upgrade message.
114
+ """
115
+ global _out_of_date
116
+ if _out_of_date is not None:
117
+ return _out_of_date
118
+
119
+ ### NOTE: Remote version is cached asynchronously.
120
+ if not UPDATES_CACHE_PATH.exists():
121
+ return False
122
+
123
+ remote_version_str = get_remote_version_from_cache()
124
+
125
+ packaging_version = mrsm.attempt_import('packaging.version')
126
+ current_version = packaging_version.parse(mrsm.__version__)
127
+ remote_version = packaging_version.parse(remote_version_str)
128
+
129
+ _out_of_date = remote_version > current_version
130
+ return _out_of_date
131
+
132
+
133
+ def get_update_message() -> str:
134
+ """
135
+ Return the formatted message for when the current version is behind the latest release.
136
+ """
137
+ if not mrsm_out_of_date():
138
+ return ''
139
+
140
+ intro = get_config('shell', CHARSET, 'intro')
141
+ update_message = get_config('shell', CHARSET, 'update_message')
142
+ remote_version = get_remote_version_from_cache()
143
+ if not remote_version:
144
+ return ''
145
+
146
+ intro_width = string_width(intro)
147
+ msg_width = string_width(update_message)
148
+ update_left_padding = ' ' * ((intro_width - msg_width) // 2)
149
+
150
+ update_line = (
151
+ colored(
152
+ update_message,
153
+ *get_config('shell', 'ansi', 'update_message', 'color')
154
+ ) if ANSI
155
+ else update_message
156
+ )
157
+ update_instruction = (
158
+ colored("Run ", 'white')
159
+ + colored("upgrade mrsm", 'green')
160
+ + colored(" to install ", 'white')
161
+ + colored(f'v{remote_version}', 'yellow')
162
+ + '.'
163
+ )
164
+ update_instruction_clean = remove_ansi(update_instruction)
165
+ instruction_width = string_width(update_instruction_clean)
166
+ instruction_left_padding = ' ' * ((intro_width - instruction_width) // 2)
167
+
168
+ return (
169
+ '\n\n'
170
+ + update_left_padding
171
+ + update_line
172
+ + '\n'
173
+ + instruction_left_padding
174
+ + update_instruction
175
+ )
@@ -22,7 +22,7 @@ tornado, tornado_ioloop, terminado = attempt_import(
22
22
 
23
23
  def get_webterm_app_and_manager() -> Tuple[
24
24
  tornado.web.Application,
25
- terminado.UniqueTermManager,
25
+ terminado.UniqueTermManager,
26
26
  ]:
27
27
  """
28
28
  Construct the Tornado web app and term manager from the provided sysargs.
@@ -47,7 +47,7 @@ def get_webterm_app_and_manager() -> Tuple[
47
47
  {'term_manager': term_manager}
48
48
  ),
49
49
  (
50
- r"/",
50
+ r"/",
51
51
  TermPageHandler
52
52
  ),
53
53
  ]
@@ -221,18 +221,17 @@ def _bootstrap_pipes(
221
221
  return (successes > 0), msg
222
222
 
223
223
  def _bootstrap_connectors(
224
- action : Optional[List[str]] = None,
225
- connector_keys : Optional[List[str]] = None,
226
- yes : bool = False,
227
- force : bool = False,
228
- noask : bool = False,
229
- debug : bool = False,
230
- return_keys : bool = False,
231
- **kw : Any
232
- ) -> Union[SuccessTuple, Tuple[str, str]]:
224
+ action: Optional[List[str]] = None,
225
+ connector_keys: Optional[List[str]] = None,
226
+ yes: bool = False,
227
+ force: bool = False,
228
+ noask: bool = False,
229
+ debug: bool = False,
230
+ return_keys: bool = False,
231
+ **kw: Any
232
+ ) -> Union[SuccessTuple, Tuple[str, str]]:
233
233
  """
234
234
  Prompt the user for the details necessary to create a Connector.
235
-
236
235
  """
237
236
  from meerschaum.connectors.parse import is_valid_connector_keys
238
237
  from meerschaum.connectors import connectors, get_connector, types, custom_types
@@ -386,10 +385,10 @@ def _bootstrap_connectors(
386
385
 
387
386
 
388
387
  def _bootstrap_plugins(
389
- action: Optional[List[str]] = None,
390
- debug: bool = False,
391
- **kwargs: Any
392
- ) -> SuccessTuple:
388
+ action: Optional[List[str]] = None,
389
+ debug: bool = False,
390
+ **kwargs: Any
391
+ ) -> SuccessTuple:
393
392
  """
394
393
  Launch an interactive wizard to guide the user to creating a new plugin.
395
394
  """
@@ -9,14 +9,14 @@ from __future__ import annotations
9
9
  from meerschaum.utils.typing import SuccessTuple, Any, List, Optional
10
10
 
11
11
  def python(
12
- action: Optional[List[str]] = None,
13
- sub_args: Optional[List[str]] = None,
14
- nopretty: bool = False,
15
- noask: bool = False,
16
- venv: Optional[str] = None,
17
- debug: bool = False,
18
- **kw: Any
19
- ) -> SuccessTuple:
12
+ action: Optional[List[str]] = None,
13
+ sub_args: Optional[List[str]] = None,
14
+ nopretty: bool = False,
15
+ noask: bool = False,
16
+ venv: Optional[str] = None,
17
+ debug: bool = False,
18
+ **kw: Any
19
+ ) -> SuccessTuple:
20
20
  """
21
21
  Launch a virtual environment's Python interpreter with Meerschaum imported.
22
22
  You may pass flags to the Python binary by surrounding each flag with `[]`.
@@ -56,6 +56,9 @@ def python(
56
56
  if action is None:
57
57
  action = []
58
58
 
59
+ if noask:
60
+ nopretty = True
61
+
59
62
  joined_actions = (
60
63
  ["import meerschaum as mrsm"]
61
64
  if venv is None and not sub_args
@@ -7,12 +7,13 @@ Register new Pipes. Requires the API to be running.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+ import meerschaum as mrsm
10
11
  from meerschaum.utils.typing import SuccessTuple, Any, List, Optional, Dict
11
12
 
12
13
  def register(
13
- action: Optional[List[str]] = None,
14
- **kw: Any
15
- ) -> SuccessTuple:
14
+ action: Optional[List[str]] = None,
15
+ **kw: Any
16
+ ) -> SuccessTuple:
16
17
  """
17
18
  Register new items (pipes, plugins, users).
18
19
 
@@ -23,22 +24,25 @@ def register(
23
24
  'pipes' : _register_pipes,
24
25
  'plugins' : _register_plugins,
25
26
  'users' : _register_users,
27
+ 'connectors': _register_connectors,
26
28
  }
27
29
  return choose_subaction(action, options, **kw)
28
30
 
29
31
 
30
32
  def _complete_register(
31
- action: Optional[List[str]] = None,
32
- **kw: Any
33
- ) -> List[str]:
33
+ action: Optional[List[str]] = None,
34
+ **kw: Any
35
+ ) -> List[str]:
34
36
  """
35
37
  Override the default Meerschaum `complete_` function.
36
38
  """
37
39
  if action is None:
38
40
  action = []
39
41
  options = {
40
- 'plugin' : _complete_register_plugins,
41
- 'plugins' : _complete_register_plugins,
42
+ 'plugin': _complete_register_plugins,
43
+ 'plugins': _complete_register_plugins,
44
+ 'connector': _complete_register_connectors,
45
+ 'connectors': _complete_register_connectors,
42
46
  }
43
47
 
44
48
  if len(action) > 0 and action[0] in options:
@@ -51,14 +55,14 @@ def _complete_register(
51
55
 
52
56
 
53
57
  def _register_pipes(
54
- connector_keys: Optional[List[str]] = None,
55
- metric_keys: Optional[List[str]] = None,
56
- location_keys: Optional[List[str]] = None,
57
- params: Optional[Dict[str, Any]] = None,
58
- tags: Optional[List[str]] = None,
59
- debug: bool = False,
60
- **kw: Any
61
- ) -> SuccessTuple:
58
+ connector_keys: Optional[List[str]] = None,
59
+ metric_keys: Optional[List[str]] = None,
60
+ location_keys: Optional[List[str]] = None,
61
+ params: Optional[Dict[str, Any]] = None,
62
+ tags: Optional[List[str]] = None,
63
+ debug: bool = False,
64
+ **kw: Any
65
+ ) -> SuccessTuple:
62
66
  """
63
67
  Create and register Pipe objects.
64
68
 
@@ -147,15 +151,18 @@ def _register_pipes(
147
151
 
148
152
 
149
153
  def _register_plugins(
150
- action: Optional[List[str]] = None,
151
- repository: Optional[str] = None,
152
- shell: bool = False,
153
- debug: bool = False,
154
- yes: bool = False,
155
- noask: bool = False,
156
- force: bool = False,
157
- **kw: Any
158
- ) -> SuccessTuple:
154
+ action: Optional[List[str]] = None,
155
+ repository: Optional[str] = None,
156
+ shell: bool = False,
157
+ debug: bool = False,
158
+ yes: bool = False,
159
+ noask: bool = False,
160
+ force: bool = False,
161
+ **kw: Any
162
+ ) -> SuccessTuple:
163
+ """
164
+ Upload plugins to an API instance (repository).
165
+ """
159
166
  from meerschaum.utils.debug import dprint
160
167
  from meerschaum.plugins import reload_plugins, get_plugins_names
161
168
  from meerschaum.connectors.parse import parse_repo_keys
@@ -196,14 +203,23 @@ def _register_plugins(
196
203
  successes = {}
197
204
 
198
205
  for name, plugin in plugins_to_register.items():
199
- desc = None
200
206
  plugin.attributes = repo_connector.get_plugin_attributes(plugin, debug=debug)
201
207
  if plugin.attributes is None:
202
208
  plugin.attributes = {}
209
+
210
+ try:
211
+ description_text = plugin.attributes.get('description', '')
212
+ doc_text = plugin.module.__doc__.lstrip().rstrip()
213
+ except Exception:
214
+ description_text = ''
215
+ doc_text = ''
216
+
217
+ desc = description_text or doc_text or ''
218
+
203
219
  question = f"Would you like to add a description to plugin '{name}'?"
204
- if plugin.attributes.get('description', None):
220
+ if desc:
205
221
  info(f"Found existing description for plugin '{plugin}':")
206
- print(plugin.attributes['description'])
222
+ print(desc)
207
223
  question = (
208
224
  "Would you like to overwrite this description?\n"
209
225
  + "To edit the existing text, visit /dash/plugins for this repository."
@@ -213,9 +229,14 @@ def _register_plugins(
213
229
  default='n',
214
230
  yes=yes
215
231
  ):
216
- info('Press (Esc + Enter) to submit the description, (CTRL + C) to cancel.')
232
+ info('Press (Esc + Enter) to submit, (CTRL + C) to cancel.')
217
233
  try:
218
- desc = prompt('', multiline=True, icon=False)
234
+ desc = prompt(
235
+ '',
236
+ multiline=True,
237
+ icon=False,
238
+ default_editable=desc.lstrip().rstrip(),
239
+ )
219
240
  except KeyboardInterrupt:
220
241
  desc = None
221
242
  if desc == '':
@@ -246,17 +267,19 @@ def _register_plugins(
246
267
  reload_plugins(debug=debug)
247
268
  return total_success > 0, msg
248
269
 
270
+
249
271
  def _complete_register_plugins(*args, **kw):
250
272
  from meerschaum.actions.uninstall import _complete_uninstall_plugins
251
273
  return _complete_uninstall_plugins(*args, **kw)
252
274
 
275
+
253
276
  def _register_users(
254
- action: Optional[List[str]] = None,
255
- mrsm_instance: Optional[str] = None,
256
- shell: bool = False,
257
- debug: bool = False,
258
- **kw: Any
259
- ) -> SuccessTuple:
277
+ action: Optional[List[str]] = None,
278
+ mrsm_instance: Optional[str] = None,
279
+ shell: bool = False,
280
+ debug: bool = False,
281
+ **kw: Any
282
+ ) -> SuccessTuple:
260
283
  """
261
284
  Register a new user to a Meerschaum instance.
262
285
  """
@@ -294,7 +317,7 @@ def _register_users(
294
317
  nonregistered_users.append(user)
295
318
 
296
319
  ### prompt for passwords and emails, then try to register
297
- success = dict()
320
+ success = {}
298
321
  successfully_registered_users = set()
299
322
  for _user in nonregistered_users:
300
323
  try:
@@ -337,6 +360,95 @@ def _register_users(
337
360
  )
338
361
  return succeeded > 0, msg
339
362
 
363
+
364
+ def _register_connectors(
365
+ action: Optional[List[str]] = None,
366
+ connector_keys: Optional[List[str]] = None,
367
+ params: Optional[Dict[str, Any]] = None,
368
+ **kwargs: Any
369
+ ) -> SuccessTuple:
370
+ """
371
+ Create new connectors programmatically with `--params`.
372
+ See `bootstrap connector`.
373
+
374
+ Examples:
375
+
376
+ mrsm register connector sql:tmp --params 'uri:sqlite:////tmp/tmp.db'
377
+
378
+ mrsm register connector -c sql:new --params '{"database": "/tmp/new.db"}'
379
+ """
380
+ from meerschaum.config import get_config, write_config
381
+ from meerschaum.utils.prompt import yes_no
382
+ from meerschaum.utils.warnings import warn
383
+ all_keys = (action or []) + (connector_keys or [])
384
+ if len(all_keys) != 1:
385
+ return (
386
+ False,
387
+ "Provide one pair of keys for the connector to be registered."
388
+ )
389
+
390
+ keys = all_keys[0]
391
+
392
+ if keys.count(':') != 1:
393
+ return False, "Connector keys must be in the format `type:label`."
394
+
395
+ type_, label = keys.split(':', maxsplit=1)
396
+ mrsm_config = get_config('meerschaum')
397
+ if 'connectors' not in mrsm_config:
398
+ mrsm_config['connectors'] = {}
399
+
400
+ if type_ not in mrsm_config['connectors']:
401
+ mrsm_config['connectors'] = {}
402
+
403
+ is_new = True
404
+ if label in mrsm_config['connectors'][type_]:
405
+ rich_table, rich_json, rich_box = mrsm.attempt_import(
406
+ 'rich.table',
407
+ 'rich.json',
408
+ 'rich.box',
409
+ )
410
+ existing_params = mrsm_config['connectors'][type_][label]
411
+ if existing_params == params:
412
+ return True, "Connector exists, nothing to do."
413
+
414
+ table = rich_table.Table(box=rich_box.MINIMAL)
415
+ table.add_column('Existing Parameters')
416
+ table.add_column('New Parameters')
417
+ table.add_row(
418
+ rich_json.JSON.from_data(existing_params),
419
+ rich_json.JSON.from_data(params or {}),
420
+ )
421
+
422
+ mrsm.pprint(table)
423
+ warn(f"Connector '{keys}' already exists.", stack=False)
424
+ if not yes_no(
425
+ f"Do you want to overwrite connector '{keys}'?",
426
+ default='n',
427
+ **kwargs
428
+ ):
429
+ return False, "Nothing was changed."
430
+
431
+ is_new = False
432
+
433
+ mrsm_config['connectors'][type_][label] = params
434
+ if not write_config({'meerschaum': mrsm_config}):
435
+ return False, "Failed to update configuration."
436
+
437
+ msg = (
438
+ "Successfully "
439
+ + ("registered" if is_new else "updated")
440
+ + f" connector '{keys}'."
441
+ )
442
+ return True, msg
443
+
444
+
445
+ def _complete_register_connectors(
446
+ action: Optional[List[str]] = None, **kw: Any
447
+ ) -> List[str]:
448
+ from meerschaum.actions.show import _complete_show_connectors
449
+ return _complete_show_connectors(action)
450
+
451
+
340
452
  ### NOTE: This must be the final statement of the module.
341
453
  ### Any subactions added below these lines will not
342
454
  ### be added to the `help` docstring.