meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc8__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 (117) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +435 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +115 -24
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +4 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +2 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +29 -0
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/register.py +9 -2
  34. meerschaum/api/dash/pages/login.py +2 -2
  35. meerschaum/api/dash/pipes.py +72 -36
  36. meerschaum/api/dash/webterm.py +14 -6
  37. meerschaum/api/models/_pipes.py +7 -1
  38. meerschaum/api/resources/static/js/terminado.js +3 -0
  39. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  40. meerschaum/api/resources/templates/termpage.html +1 -0
  41. meerschaum/api/routes/_jobs.py +23 -11
  42. meerschaum/api/routes/_login.py +73 -5
  43. meerschaum/api/routes/_pipes.py +6 -4
  44. meerschaum/api/routes/_webterm.py +3 -3
  45. meerschaum/config/__init__.py +60 -13
  46. meerschaum/config/_default.py +89 -61
  47. meerschaum/config/_edit.py +10 -8
  48. meerschaum/config/_formatting.py +2 -0
  49. meerschaum/config/_patch.py +4 -2
  50. meerschaum/config/_paths.py +127 -12
  51. meerschaum/config/_read_config.py +32 -12
  52. meerschaum/config/_version.py +1 -1
  53. meerschaum/config/environment.py +262 -0
  54. meerschaum/config/stack/__init__.py +7 -5
  55. meerschaum/connectors/_Connector.py +1 -2
  56. meerschaum/connectors/__init__.py +37 -2
  57. meerschaum/connectors/api/_APIConnector.py +1 -1
  58. meerschaum/connectors/api/_jobs.py +11 -0
  59. meerschaum/connectors/api/_pipes.py +7 -1
  60. meerschaum/connectors/instance/_plugins.py +9 -1
  61. meerschaum/connectors/instance/_tokens.py +20 -3
  62. meerschaum/connectors/instance/_users.py +8 -1
  63. meerschaum/connectors/parse.py +1 -1
  64. meerschaum/connectors/sql/_create_engine.py +3 -0
  65. meerschaum/connectors/sql/_pipes.py +93 -79
  66. meerschaum/connectors/sql/_users.py +8 -1
  67. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  68. meerschaum/connectors/valkey/_pipes.py +7 -5
  69. meerschaum/core/Pipe/__init__.py +45 -71
  70. meerschaum/core/Pipe/_attributes.py +66 -90
  71. meerschaum/core/Pipe/_cache.py +555 -0
  72. meerschaum/core/Pipe/_clear.py +0 -11
  73. meerschaum/core/Pipe/_data.py +0 -50
  74. meerschaum/core/Pipe/_deduplicate.py +0 -13
  75. meerschaum/core/Pipe/_delete.py +12 -21
  76. meerschaum/core/Pipe/_drop.py +11 -23
  77. meerschaum/core/Pipe/_dtypes.py +1 -1
  78. meerschaum/core/Pipe/_index.py +8 -14
  79. meerschaum/core/Pipe/_sync.py +12 -18
  80. meerschaum/core/Plugin/_Plugin.py +7 -1
  81. meerschaum/core/Token/_Token.py +1 -1
  82. meerschaum/core/User/_User.py +1 -2
  83. meerschaum/jobs/_Executor.py +88 -4
  84. meerschaum/jobs/_Job.py +146 -36
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +197 -42
  88. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  89. meerschaum/utils/daemon/RotatingFile.py +63 -36
  90. meerschaum/utils/daemon/StdinFile.py +53 -13
  91. meerschaum/utils/daemon/__init__.py +18 -5
  92. meerschaum/utils/daemon/_names.py +6 -3
  93. meerschaum/utils/debug.py +34 -4
  94. meerschaum/utils/dtypes/__init__.py +5 -1
  95. meerschaum/utils/formatting/__init__.py +4 -1
  96. meerschaum/utils/formatting/_jobs.py +1 -1
  97. meerschaum/utils/formatting/_pipes.py +47 -46
  98. meerschaum/utils/formatting/_shell.py +33 -9
  99. meerschaum/utils/misc.py +22 -38
  100. meerschaum/utils/packages/__init__.py +15 -13
  101. meerschaum/utils/packages/_packages.py +1 -0
  102. meerschaum/utils/pipes.py +33 -5
  103. meerschaum/utils/process.py +1 -1
  104. meerschaum/utils/prompt.py +172 -143
  105. meerschaum/utils/sql.py +12 -2
  106. meerschaum/utils/threading.py +42 -0
  107. meerschaum/utils/venv/__init__.py +2 -0
  108. meerschaum/utils/warnings.py +19 -13
  109. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/zip-safe +0 -0
@@ -8,6 +8,7 @@ Expose plugin management APIs from the `meerschaum.plugins` module.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import pathlib
11
12
  import functools
12
13
 
13
14
  import meerschaum as mrsm
@@ -16,13 +17,15 @@ from meerschaum.utils.threading import Lock, RLock
16
17
  from meerschaum.core.Plugin import Plugin
17
18
 
18
19
  _api_plugins: Dict[str, List[Callable[['fastapi.App'], Any]]] = {}
19
- _pre_sync_hooks: Dict[str, List[Callable[[Any], Any]]] = {}
20
- _post_sync_hooks: Dict[str, List[Callable[[Any], Any]]] = {}
20
+ _pre_sync_hooks: Dict[Union[str, None], List[Callable[[Any], Any]]] = {}
21
+ _post_sync_hooks: Dict[Union[str, None], List[Callable[[Any], Any]]] = {}
22
+ _actions_daemon_enabled: Dict[str, bool] = {}
21
23
  _locks = {
22
24
  '_api_plugins': RLock(),
23
25
  '_dash_plugins': RLock(),
24
26
  '_pre_sync_hooks': RLock(),
25
27
  '_post_sync_hooks': RLock(),
28
+ '_actions_daemon_enabled': RLock(),
26
29
  '__path__': RLock(),
27
30
  'sys.path': RLock(),
28
31
  'internal_plugins': RLock(),
@@ -53,11 +56,14 @@ __pdoc__ = {
53
56
 
54
57
 
55
58
  def make_action(
56
- function: Callable[[Any], Any],
59
+ function: Optional[Callable[[Any], Any]] = None,
57
60
  shell: bool = False,
58
61
  activate: bool = True,
59
62
  deactivate: bool = True,
60
- debug: bool = False
63
+ debug: bool = False,
64
+ daemon: bool = True,
65
+ skip_if_loaded: bool = True,
66
+ _plugin_name: Optional[str] = None,
61
67
  ) -> Callable[[Any], Any]:
62
68
  """
63
69
  Make a function a Meerschaum action. Useful for plugins that are adding multiple actions.
@@ -84,25 +90,36 @@ def make_action(
84
90
  ... return True, "Success"
85
91
  >>>
86
92
  """
93
+ def _decorator(func: Callable[[Any], Any]) -> Callable[[Any], Any]:
94
+ from meerschaum.actions import actions, _custom_actions_plugins, _plugins_actions
95
+ if skip_if_loaded and func.__name__ in actions:
96
+ return func
87
97
 
88
- from meerschaum.actions import actions
89
- from meerschaum.utils.formatting import pprint
90
- package_name = function.__globals__['__name__']
91
- plugin_name = (
92
- package_name.split('.')[1]
93
- if package_name.startswith('plugins.') else None
94
- )
95
- plugin = Plugin(plugin_name) if plugin_name else None
96
-
97
- if debug:
98
- from meerschaum.utils.debug import dprint
99
- dprint(
100
- f"Adding action '{function.__name__}' from plugin " +
101
- f"'{plugin}'..."
98
+ from meerschaum.config.paths import PLUGINS_RESOURCES_PATH
99
+ plugin_name = (
100
+ func.__name__.split(f"{PLUGINS_RESOURCES_PATH.stem}.", maxsplit=1)[-1].split('.')[0]
102
101
  )
102
+ plugin = Plugin(plugin_name) if plugin_name else None
103
103
 
104
- actions[function.__name__] = function
105
- return function
104
+ if debug:
105
+ from meerschaum.utils.debug import dprint
106
+ dprint(
107
+ f"Adding action '{func.__name__}' from plugin "
108
+ f"'{plugin}'..."
109
+ )
110
+
111
+ actions[func.__name__] = func
112
+ _custom_actions_plugins[func.__name__] = plugin_name
113
+ if plugin_name not in _plugins_actions:
114
+ _plugins_actions[plugin_name] = []
115
+ _plugins_actions[plugin_name].append(func.__name__)
116
+ if not daemon:
117
+ _actions_daemon_enabled[func.__name__] = False
118
+ return func
119
+
120
+ if function is None:
121
+ return _decorator
122
+ return _decorator(function)
106
123
 
107
124
 
108
125
  def pre_sync_hook(
@@ -130,10 +147,11 @@ def pre_sync_hook(
130
147
  >>>
131
148
  """
132
149
  with _locks['_pre_sync_hooks']:
150
+ plugin_name = _get_parent_plugin()
133
151
  try:
134
- if function.__module__ not in _pre_sync_hooks:
135
- _pre_sync_hooks[function.__module__] = []
136
- _pre_sync_hooks[function.__module__].append(function)
152
+ if plugin_name not in _pre_sync_hooks:
153
+ _pre_sync_hooks[plugin_name] = []
154
+ _pre_sync_hooks[plugin_name].append(function)
137
155
  except Exception as e:
138
156
  from meerschaum.utils.warnings import warn
139
157
  warn(e)
@@ -170,9 +188,10 @@ def post_sync_hook(
170
188
  """
171
189
  with _locks['_post_sync_hooks']:
172
190
  try:
173
- if function.__module__ not in _post_sync_hooks:
174
- _post_sync_hooks[function.__module__] = []
175
- _post_sync_hooks[function.__module__].append(function)
191
+ plugin_name = _get_parent_plugin()
192
+ if plugin_name not in _post_sync_hooks:
193
+ _post_sync_hooks[plugin_name] = []
194
+ _post_sync_hooks[plugin_name].append(function)
176
195
  except Exception as e:
177
196
  from meerschaum.utils.warnings import warn
178
197
  warn(e)
@@ -180,6 +199,7 @@ def post_sync_hook(
180
199
 
181
200
 
182
201
  _plugin_endpoints_to_pages = {}
202
+ _plugins_web_pages = {}
183
203
  def web_page(
184
204
  page: Union[str, None, Callable[[Any], Any]] = None,
185
205
  login_required: bool = True,
@@ -226,11 +246,7 @@ def web_page(
226
246
  )
227
247
  )
228
248
 
229
- package_name = _func.__globals__['__name__']
230
- plugin_name = (
231
- package_name.split('.')[1]
232
- if package_name.startswith('plugins.') else None
233
- )
249
+ plugin_name = _get_parent_plugin()
234
250
  page_group = page_group or plugin_name
235
251
  if page_group not in _plugin_endpoints_to_pages:
236
252
  _plugin_endpoints_to_pages[page_group] = {}
@@ -240,6 +256,9 @@ def web_page(
240
256
  'skip_navbar': skip_navbar,
241
257
  'page_key': page_key,
242
258
  }
259
+ if plugin_name not in _plugins_web_pages:
260
+ _plugins_web_pages[plugin_name] = []
261
+ _plugins_web_pages[plugin_name].append(_func)
243
262
  return wrapper
244
263
 
245
264
  if callable(page):
@@ -258,10 +277,11 @@ def dash_plugin(function: Callable[[Any], Any]) -> Callable[[Any], Any]:
258
277
  Execute the function when starting the Dash application.
259
278
  """
260
279
  with _locks['_dash_plugins']:
280
+ plugin_name = _get_parent_plugin()
261
281
  try:
262
- if function.__module__ not in _dash_plugins:
263
- _dash_plugins[function.__module__] = []
264
- _dash_plugins[function.__module__].append(function)
282
+ if plugin_name not in _dash_plugins:
283
+ _dash_plugins[plugin_name] = []
284
+ _dash_plugins[plugin_name].append(function)
265
285
  except Exception as e:
266
286
  from meerschaum.utils.warnings import warn
267
287
  warn(e)
@@ -299,16 +319,19 @@ def api_plugin(function: Callable[[Any], Any]) -> Callable[[Any], Any]:
299
319
 
300
320
 
301
321
  _synced_symlinks: bool = False
322
+ _injected_plugin_symlinks = set()
302
323
  def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
303
324
  """
304
- Update the plugins
325
+ Update the plugins' internal symlinks.
305
326
  """
306
327
  global _synced_symlinks
307
328
  with _locks['_synced_symlinks']:
308
329
  if _synced_symlinks:
309
330
  return
310
331
 
311
- import sys, os, pathlib, time
332
+ import os
333
+ import pathlib
334
+ import time
312
335
  from collections import defaultdict
313
336
  import importlib.util
314
337
  from meerschaum.utils.misc import flatten_list, make_symlink, is_symlink
@@ -321,6 +344,7 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
321
344
  PLUGINS_INIT_PATH,
322
345
  PLUGINS_DIR_PATHS,
323
346
  PLUGINS_INTERNAL_LOCK_PATH,
347
+ PLUGINS_INJECTED_RESOURCES_PATH,
324
348
  )
325
349
 
326
350
  ### If the lock file exists, sleep for up to a second or until it's removed before continuing.
@@ -354,7 +378,7 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
354
378
  try:
355
379
  from importlib.metadata import entry_points
356
380
  except ImportError:
357
- importlib_metadata = attempt_import('importlib_metadata', lazy=False)
381
+ importlib_metadata = mrsm.attempt_import('importlib_metadata', lazy=False)
358
382
  entry_points = importlib_metadata.entry_points
359
383
 
360
384
  ### NOTE: Allow plugins to be installed via `pip`.
@@ -382,23 +406,27 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
382
406
 
383
407
  PLUGINS_RESOURCES_PATH.mkdir(exist_ok=True)
384
408
 
385
- existing_symlinked_paths = [
386
- (PLUGINS_RESOURCES_PATH / item)
409
+ existing_symlinked_paths = {
410
+ _existing_symlink: pathlib.Path(os.path.realpath(_existing_symlink))
387
411
  for item in os.listdir(PLUGINS_RESOURCES_PATH)
388
- ]
389
- for plugins_path in PLUGINS_DIR_PATHS:
390
- if not plugins_path.exists():
391
- plugins_path.mkdir(exist_ok=True, parents=True)
412
+ if is_symlink(_existing_symlink := (PLUGINS_RESOURCES_PATH / item))
413
+ }
414
+ injected_symlinked_paths = {
415
+ _injected_symlink: pathlib.Path(os.path.realpath(_injected_symlink))
416
+ for item in os.listdir(PLUGINS_INJECTED_RESOURCES_PATH)
417
+ if is_symlink(_injected_symlink := (PLUGINS_INJECTED_RESOURCES_PATH / item))
418
+ }
392
419
  plugins_to_be_symlinked = list(flatten_list(
393
420
  [
394
421
  [
395
- (plugins_path / item)
422
+ pathlib.Path(os.path.realpath(plugins_path / item))
396
423
  for item in os.listdir(plugins_path)
397
424
  if (
398
425
  not item.startswith('.')
399
426
  ) and (item not in ('__pycache__', '__init__.py'))
400
427
  ]
401
428
  for plugins_path in PLUGINS_DIR_PATHS
429
+ if plugins_path.exists()
402
430
  ]
403
431
  ))
404
432
  plugins_to_be_symlinked.extend(packaged_plugin_paths)
@@ -413,16 +441,19 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
413
441
  if warn:
414
442
  _warn(f"Found duplicate plugins named '{plugin_name}'.")
415
443
 
416
- for plugin_symlink_path in existing_symlinked_paths:
417
- real_path = pathlib.Path(os.path.realpath(plugin_symlink_path))
444
+ for plugin_symlink_path, real_path in existing_symlinked_paths.items():
418
445
 
419
446
  ### Remove invalid symlinks.
420
447
  if real_path not in plugins_to_be_symlinked:
421
- if not is_symlink(plugin_symlink_path):
448
+ if plugin_symlink_path in _injected_plugin_symlinks:
449
+ continue
450
+ if plugin_symlink_path in injected_symlinked_paths:
451
+ continue
452
+ if real_path in injected_symlinked_paths.values():
422
453
  continue
423
454
  try:
424
455
  plugin_symlink_path.unlink()
425
- except Exception as e:
456
+ except Exception:
426
457
  pass
427
458
 
428
459
  ### Remove valid plugins from the to-be-symlinked list.
@@ -499,13 +530,13 @@ def import_plugins(
499
530
 
500
531
  """
501
532
  import sys
502
- import os
503
533
  import importlib
504
534
  from meerschaum.utils.misc import flatten_list
505
535
  from meerschaum.config._paths import PLUGINS_RESOURCES_PATH
506
536
  from meerschaum.utils.venv import is_venv_active, activate_venv, deactivate_venv, Venv
507
537
  from meerschaum.utils.warnings import warn as _warn
508
538
  plugins_to_import = list(plugins_to_import)
539
+ prepended_sys_path = False
509
540
  with _locks['sys.path']:
510
541
 
511
542
  ### Since plugins may depend on other plugins,
@@ -518,6 +549,7 @@ def import_plugins(
518
549
  already_active_venvs = [is_venv_active(plugin_name) for plugin_name in plugins_names]
519
550
 
520
551
  if not sys.path or sys.path[0] != str(PLUGINS_RESOURCES_PATH.parent):
552
+ prepended_sys_path = True
521
553
  sys.path.insert(0, str(PLUGINS_RESOURCES_PATH.parent))
522
554
 
523
555
  if not plugins_to_import:
@@ -563,9 +595,9 @@ def import_plugins(
563
595
  imported_plugins.append(None)
564
596
 
565
597
  if imported_plugins is None and warn:
566
- _warn(f"Failed to import plugins.", stacklevel=3)
598
+ _warn("Failed to import plugins.", stacklevel=3)
567
599
 
568
- if str(PLUGINS_RESOURCES_PATH.parent) in sys.path:
600
+ if prepended_sys_path and str(PLUGINS_RESOURCES_PATH.parent) in sys.path:
569
601
  sys.path.remove(str(PLUGINS_RESOURCES_PATH.parent))
570
602
 
571
603
  if isinstance(imported_plugins, list):
@@ -623,7 +655,7 @@ def from_plugin_import(plugin_import_name: str, *attrs: str) -> Any:
623
655
  attrs_to_return = []
624
656
  with mrsm.Venv(plugin):
625
657
  if plugin.module is None:
626
- return None
658
+ raise ImportError(f"Unable to import plugin '{plugin}'.")
627
659
 
628
660
  try:
629
661
  submodule = importlib.import_module(submodule_import_name)
@@ -656,16 +688,23 @@ def from_plugin_import(plugin_import_name: str, *attrs: str) -> Any:
656
688
  return tuple(attrs_to_return)
657
689
 
658
690
 
659
- def load_plugins(debug: bool = False, shell: bool = False) -> None:
691
+ _loaded_plugins: bool = False
692
+ def load_plugins(
693
+ skip_if_loaded: bool = True,
694
+ shell: bool = False,
695
+ debug: bool = False,
696
+ ) -> None:
660
697
  """
661
698
  Import Meerschaum plugins and update the actions dictionary.
662
699
  """
700
+ global _loaded_plugins
701
+ if skip_if_loaded and _loaded_plugins:
702
+ return
703
+
663
704
  from inspect import isfunction, getmembers
664
705
  from meerschaum.actions import __all__ as _all, modules
665
706
  from meerschaum.config._paths import PLUGINS_RESOURCES_PATH
666
707
  from meerschaum.utils.packages import get_modules_from_package
667
- if debug:
668
- from meerschaum.utils.debug import dprint
669
708
 
670
709
  _plugins_names, plugins_modules = get_modules_from_package(
671
710
  import_plugins(),
@@ -676,7 +715,8 @@ def load_plugins(debug: bool = False, shell: bool = False) -> None:
676
715
  ### I'm appending here to keep from redefining the modules list.
677
716
  new_modules = (
678
717
  [
679
- mod for mod in modules
718
+ mod
719
+ for mod in modules
680
720
  if not mod.__name__.startswith(PLUGINS_RESOURCES_PATH.stem + '.')
681
721
  ]
682
722
  + plugins_modules
@@ -692,7 +732,120 @@ def load_plugins(debug: bool = False, shell: bool = False) -> None:
692
732
  if not isfunction(func):
693
733
  continue
694
734
  if name == module.__name__.split('.')[-1]:
695
- make_action(func, **{'shell': shell, 'debug': debug})
735
+ make_action(
736
+ func,
737
+ **{'shell': shell, 'debug': debug},
738
+ _plugin_name=name,
739
+ skip_if_loaded=True,
740
+ )
741
+
742
+ _loaded_plugins = True
743
+
744
+
745
+ def unload_custom_actions(plugins: Optional[List[str]] = None, debug: bool = False) -> None:
746
+ """
747
+ Unload the custom actions added by plugins.
748
+ """
749
+ from meerschaum.actions import (
750
+ actions,
751
+ _custom_actions_plugins,
752
+ _plugins_actions,
753
+ )
754
+ from meerschaum._internal.entry import _shell
755
+ import meerschaum._internal.shell as shell_pkg
756
+
757
+ plugins = plugins or list(_plugins_actions.keys())
758
+
759
+ for plugin in plugins:
760
+ action_names = _plugins_actions.get(plugin, [])
761
+ actions_to_remove = {
762
+ action_name: actions.get(action_name, None)
763
+ for action_name in action_names
764
+ }
765
+ for action_name in action_names:
766
+ _ = actions.pop(action_name, None)
767
+ _ = _custom_actions_plugins.pop(action_name, None)
768
+ _ = _actions_daemon_enabled.pop(action_name, None)
769
+
770
+ _ = _plugins_actions.pop(plugin, None)
771
+ shell_pkg._remove_shell_actions(
772
+ _shell=_shell,
773
+ actions=actions_to_remove,
774
+ )
775
+
776
+
777
+ def unload_plugins(
778
+ plugins: Optional[List[str]] = None,
779
+ remove_symlinks: bool = True,
780
+ debug: bool = False,
781
+ ) -> None:
782
+ """
783
+ Unload the specified plugins from memory.
784
+ """
785
+ global _loaded_plugins
786
+ import sys
787
+ from meerschaum.config.paths import PLUGINS_RESOURCES_PATH, PLUGINS_INJECTED_RESOURCES_PATH
788
+ from meerschaum.connectors import unload_plugin_connectors
789
+ if debug:
790
+ from meerschaum.utils.warnings import dprint
791
+
792
+ _loaded_plugins = False
793
+
794
+ plugins = plugins or get_plugins_names()
795
+ if debug:
796
+ dprint(f"Unloading plugins: {plugins}")
797
+
798
+ unload_custom_actions(plugins, debug=debug)
799
+ unload_plugin_connectors(plugins, debug=debug)
800
+
801
+ module_prefix = f"{PLUGINS_RESOURCES_PATH.stem}."
802
+ loaded_modules = [mod_name for mod_name in sys.modules if mod_name.startswith(module_prefix)]
803
+
804
+ for plugin_name in plugins:
805
+ for mod_name in loaded_modules:
806
+ if mod_name[len(PLUGINS_RESOURCES_PATH.stem):].startswith(plugin_name):
807
+ _ = sys.modules.pop(mod_name, None)
808
+
809
+ ### Unload sync hooks.
810
+ _ = _pre_sync_hooks.pop(plugin_name, None)
811
+ _ = _post_sync_hooks.pop(plugin_name, None)
812
+
813
+ ### Unload API endpoints and pages.
814
+ _ = _dash_plugins.pop(plugin_name, None)
815
+ web_page_funcs = _plugins_web_pages.pop(plugin_name, None) or []
816
+ page_groups_to_pop = []
817
+ for page_group, page_functions in _plugin_endpoints_to_pages.items():
818
+ page_functions_to_pop = [
819
+ page_str
820
+ for page_str, page_payload in page_functions.items()
821
+ if page_payload.get('function', None) in web_page_funcs
822
+ ]
823
+ for page_str in page_functions_to_pop:
824
+ page_functions.pop(page_str, None)
825
+ if not page_functions:
826
+ page_groups_to_pop.append(page_group)
827
+
828
+ for page_group in page_groups_to_pop:
829
+ _plugin_endpoints_to_pages.pop(page_group, None)
830
+
831
+ ### Remove all but injected symlinks.
832
+ if remove_symlinks:
833
+ dir_symlink_path = PLUGINS_RESOURCES_PATH / plugin_name
834
+ dir_symlink_injected_path = PLUGINS_INJECTED_RESOURCES_PATH / plugin_name
835
+ file_symlink_path = PLUGINS_RESOURCES_PATH / f"{plugin_name}.py"
836
+ file_symlink_injected_path = PLUGINS_INJECTED_RESOURCES_PATH / f"{plugin_name}.py"
837
+
838
+ try:
839
+ if dir_symlink_path.exists() and not dir_symlink_injected_path.exists():
840
+ dir_symlink_path.unlink()
841
+ except Exception:
842
+ pass
843
+
844
+ try:
845
+ if file_symlink_path.exists() and not file_symlink_injected_path.exists():
846
+ file_symlink_path.unlink()
847
+ except Exception:
848
+ pass
696
849
 
697
850
 
698
851
  def reload_plugins(plugins: Optional[List[str]] = None, debug: bool = False) -> None:
@@ -705,19 +858,11 @@ def reload_plugins(plugins: Optional[List[str]] = None, debug: bool = False) ->
705
858
  The plugins to reload. `None` will reload all plugins.
706
859
 
707
860
  """
708
- import sys
709
- if debug:
710
- from meerschaum.utils.debug import dprint
711
-
712
- if not plugins:
713
- plugins = get_plugins_names()
714
- for plugin_name in plugins:
715
- if debug:
716
- dprint(f"Reloading plugin '{plugin_name}'...")
717
- mod_name = 'plugins.' + str(plugin_name)
718
- if mod_name in sys.modules:
719
- del sys.modules[mod_name]
720
- load_plugins(debug=debug)
861
+ global _synced_symlinks
862
+ unload_plugins(plugins, debug=debug)
863
+ _synced_symlinks = False
864
+ sync_plugins_symlinks(debug=debug)
865
+ load_plugins(skip_if_loaded=False, debug=debug)
721
866
 
722
867
 
723
868
  def get_plugins(*to_load, try_import: bool = True) -> Union[Tuple[Plugin], Plugin]:
@@ -737,7 +882,8 @@ def get_plugins(*to_load, try_import: bool = True) -> Union[Tuple[Plugin], Plugi
737
882
  import os
738
883
  sync_plugins_symlinks()
739
884
  _plugins = [
740
- Plugin(name) for name in (
885
+ Plugin(name)
886
+ for name in (
741
887
  to_load or [
742
888
  (
743
889
  name if (PLUGINS_RESOURCES_PATH / name).is_dir()
@@ -797,8 +943,8 @@ def add_plugin_argument(*args, **kwargs) -> None:
797
943
  >>>
798
944
  """
799
945
  from meerschaum._internal.arguments._parser import groups, _seen_plugin_args, parser
800
- from meerschaum.utils.warnings import warn, error
801
- _parent_plugin_name = _get_parent_plugin(2)
946
+ from meerschaum.utils.warnings import warn
947
+ _parent_plugin_name = _get_parent_plugin()
802
948
  title = f"Plugin '{_parent_plugin_name}' options" if _parent_plugin_name else 'Custom options'
803
949
  group_key = 'plugin_' + (_parent_plugin_name or '')
804
950
  if group_key not in groups:
@@ -814,12 +960,62 @@ def add_plugin_argument(*args, **kwargs) -> None:
814
960
  warn(e)
815
961
 
816
962
 
817
- def _get_parent_plugin(stacklevel: int = 1) -> Union[str, None]:
963
+ def inject_plugin_path(
964
+ plugin_path: pathlib.Path,
965
+ plugins_resources_path: Optional[pathlib.Path] = None) -> None:
966
+ """
967
+ Inject a plugin as a symlink into the internal `plugins` directory.
968
+
969
+ Parameters
970
+ ----------
971
+ plugin_path: pathlib.Path
972
+ The path to the plugin's source module.
973
+ """
974
+ from meerschaum.utils.misc import make_symlink
975
+ if plugins_resources_path is None:
976
+ from meerschaum.config.paths import PLUGINS_RESOURCES_PATH, PLUGINS_INJECTED_RESOURCES_PATH
977
+ plugins_resources_path = PLUGINS_RESOURCES_PATH
978
+ plugins_injected_resources_path = PLUGINS_INJECTED_RESOURCES_PATH
979
+ else:
980
+ plugins_injected_resources_path = plugins_resources_path / '.injected'
981
+
982
+ if plugin_path.is_dir():
983
+ plugin_name = plugin_path.name
984
+ dest_path = plugins_resources_path / plugin_name
985
+ injected_path = plugins_injected_resources_path / plugin_name
986
+ elif plugin_path.name == '__init__.py':
987
+ plugin_name = plugin_path.parent.name
988
+ dest_path = plugins_resources_path / plugin_name
989
+ injected_path = plugins_injected_resources_path / plugin_name
990
+ elif plugin_path.name.endswith('.py'):
991
+ plugin_name = plugin_path.name[:(-1 * len('.py'))]
992
+ dest_path = plugins_resources_path / plugin_path.name
993
+ injected_path = plugins_injected_resources_path / plugin_path.name
994
+ else:
995
+ raise ValueError(f"Cannot deduce plugin name from path '{plugin_path}'.")
996
+
997
+ _injected_plugin_symlinks.add(dest_path)
998
+ make_symlink(plugin_path, dest_path)
999
+ make_symlink(plugin_path, injected_path)
1000
+
1001
+
1002
+ def _get_parent_plugin(stacklevel: Union[int, Tuple[int, ...]] = (1, 2, 3, 4)) -> Union[str, None]:
818
1003
  """If this function is called from outside a Meerschaum plugin, it will return None."""
819
- import inspect, re
820
- try:
821
- parent_globals = inspect.stack()[stacklevel][0].f_globals
822
- parent_file = parent_globals.get('__file__', '')
823
- return parent_globals['__name__'].replace('plugins.', '').split('.')[0]
824
- except Exception as e:
825
- return None
1004
+ import inspect
1005
+ if not isinstance(stacklevel, tuple):
1006
+ stacklevel = (stacklevel,)
1007
+
1008
+ for _level in stacklevel:
1009
+ try:
1010
+ parent_globals = inspect.stack()[_level][0].f_globals
1011
+ global_name = parent_globals.get('__name__', '')
1012
+ if global_name.startswith('meerschaum.'):
1013
+ continue
1014
+ plugin_name = global_name.replace('plugins.', '').split('.')[0]
1015
+ if plugin_name.startswith('_') or plugin_name == 'importlib':
1016
+ continue
1017
+ return plugin_name
1018
+ except Exception:
1019
+ continue
1020
+
1021
+ return None