meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc7__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 +434 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +113 -19
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +3 -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 +20 -10
  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 +135 -35
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +195 -41
  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 +16 -6
  99. meerschaum/utils/misc.py +18 -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 +171 -144
  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.0rc7.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/zip-safe +0 -0
@@ -31,6 +31,7 @@ prompt_toolkit = attempt_import('prompt_toolkit', lazy=False, warn=False, instal
31
31
  )
32
32
  from meerschaum._internal.shell.ValidAutoSuggest import ValidAutoSuggest
33
33
  from meerschaum._internal.shell.ShellCompleter import ShellCompleter
34
+ from meerschaum._internal.cli.daemons import get_cli_daemon, get_cli_session_id
34
35
  _clear_screen = get_config('shell', 'clear_screen', patch=True)
35
36
  from meerschaum.utils.misc import string_width, remove_ansi
36
37
  from meerschaum.utils.warnings import warn
@@ -81,6 +82,7 @@ ESCAPED_AND_KEY: str = STATIC_CONFIG['system']['arguments']['escaped_and_key']
81
82
  PIPELINE_KEY: str = STATIC_CONFIG['system']['arguments']['pipeline_key']
82
83
  ESCAPED_PIPELINE_KEY: str = STATIC_CONFIG['system']['arguments']['escaped_pipeline_key']
83
84
 
85
+
84
86
  def _insert_shell_actions(
85
87
  _shell: Optional['Shell'] = None,
86
88
  actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
@@ -99,6 +101,10 @@ def _insert_shell_actions(
99
101
  _shell_class = _shell if _shell is not None else shell_pkg.Shell
100
102
 
101
103
  for a, f in actions.items():
104
+ existing_method = getattr(_shell_class, 'do_' + a, None)
105
+ if existing_method == f:
106
+ continue
107
+
102
108
  add_method_to_class(
103
109
  func = f,
104
110
  class_def = _shell_class,
@@ -113,6 +119,35 @@ def _insert_shell_actions(
113
119
  setattr(_shell_class, 'complete_' + a, completer)
114
120
 
115
121
 
122
+ def _remove_shell_actions(
123
+ _shell: Optional['Shell'] = None,
124
+ actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
125
+ ) -> None:
126
+ """
127
+ Remove the actions added to the shell.
128
+ """
129
+ import meerschaum._internal.shell as shell_pkg
130
+ if actions is None:
131
+ from meerschaum.actions import actions as _actions
132
+ actions = _actions
133
+
134
+ _shell_class = _shell if _shell is not None else shell_pkg.Shell
135
+
136
+ for a, f in actions.items():
137
+ try:
138
+ delattr(_shell_class, 'do_' + a)
139
+ except AttributeError:
140
+ pass
141
+
142
+ if a in reserved_completers:
143
+ continue
144
+
145
+ try:
146
+ delattr(_shell_class, 'complete_' + a)
147
+ except AttributeError:
148
+ pass
149
+
150
+
116
151
  def _completer_wrapper(
117
152
  target: Callable[[Any], List[str]]
118
153
  ) -> Callable[['mrsm._internal.shell.Shell', str, str, int, int], Any]:
@@ -294,16 +329,19 @@ class Shell(cmd.Cmd):
294
329
  pass
295
330
 
296
331
  ### NOTE: custom actions must be added to the self._actions dictionary
332
+ shell_attrs['_session_id'] = get_cli_session_id()
297
333
  shell_attrs['_actions'] = actions
298
334
  shell_attrs['_sysargs'] = sysargs
299
335
  shell_attrs['_actions']['instance'] = self.do_instance
300
336
  shell_attrs['_actions']['repo'] = self.do_repo
301
337
  shell_attrs['_actions']['executor'] = self.do_executor
302
338
  shell_attrs['_actions']['debug'] = self.do_debug
339
+ shell_attrs['_actions']['daemon'] = self.do_daemon
303
340
  shell_attrs['_update_bottom_toolbar'] = True
304
341
  shell_attrs['_old_bottom_toolbar'] = ''
305
342
  shell_attrs['debug'] = False
306
343
  shell_attrs['_reload'] = True
344
+ shell_attrs['daemon'] = get_config('system', 'experimental', 'cli_daemon')
307
345
  self.load_config(instance=instance_keys)
308
346
  self.hidden_commands = []
309
347
  ### update hidden commands list (cmd2 only)
@@ -353,7 +391,7 @@ class Shell(cmd.Cmd):
353
391
  shell_attrs['instance'] = instance
354
392
  shell_attrs['instance_keys'] = remove_ansi(str(instance))
355
393
  if shell_attrs.get('repo_keys', None) is None:
356
- shell_attrs['repo_keys'] = get_config('meerschaum', 'default_repository', patch=patch)
394
+ shell_attrs['repo_keys'] = get_config('meerschaum', 'repository', patch=patch)
357
395
  if shell_attrs.get('executor_keys', None) is None:
358
396
  shell_attrs['executor_keys'] = get_executor_keys_from_context()
359
397
 
@@ -618,7 +656,7 @@ class Shell(cmd.Cmd):
618
656
  if key == 'mrsm_instance':
619
657
  default_value = get_config('meerschaum', 'instance')
620
658
  elif key == 'repository':
621
- default_value = get_config('meerschaum', 'default_repository')
659
+ default_value = get_config('meerschaum', 'repository')
622
660
  elif key == 'executor_keys':
623
661
  default_value = get_executor_keys_from_context()
624
662
  else:
@@ -672,7 +710,12 @@ class Shell(cmd.Cmd):
672
710
  ([':'] + pipeline_args) if pipeline_args else []
673
711
  )
674
712
  try:
675
- success_tuple = entry(sysargs_to_execute, _patch_args=patch_args)
713
+ success_tuple = entry(
714
+ sysargs_to_execute,
715
+ _patch_args=patch_args,
716
+ _session_id=shell_attrs.get('session_id', None),
717
+ _use_cli_daemon=shell_attrs.get('daemon', False),
718
+ )
676
719
  except Exception as e:
677
720
  success_tuple = False, str(e)
678
721
 
@@ -732,6 +775,35 @@ class Shell(cmd.Cmd):
732
775
 
733
776
  info(f"Debug mode is {'on' if shell_attrs['debug'] else 'off'}.")
734
777
 
778
+ def do_daemon(self, action: Optional[List[str]] = None, executor_keys=None, **kw):
779
+ """
780
+ Toggle whether to route commands through the CLI daemon.
781
+
782
+ Command:
783
+ `debug {on/true | off/false}`
784
+ Ommitting on / off will toggle the existing value.
785
+ """
786
+ from meerschaum.utils.warnings import info
787
+ on_commands = {'on', 'true', 'True'}
788
+ off_commands = {'off', 'false', 'False'}
789
+ if action is None:
790
+ action = []
791
+ try:
792
+ state = action[0]
793
+ except (IndexError, AttributeError):
794
+ state = ''
795
+ if state == '':
796
+ shell_attrs['daemon'] = not shell_attrs['daemon']
797
+ elif state.lower() in on_commands:
798
+ shell_attrs['daemon'] = True
799
+ elif state.lower() in off_commands:
800
+ shell_attrs['daemon'] = False
801
+ else:
802
+ info(f"Unknown state '{state}'. Ignoring...")
803
+
804
+ info(f"CLI daemon mode is {'on' if shell_attrs['daemon'] else 'off'}.")
805
+ return True, "Success"
806
+
735
807
  def do_instance(
736
808
  self,
737
809
  action: Optional[List[str]] = None,
@@ -833,7 +905,7 @@ class Shell(cmd.Cmd):
833
905
  """
834
906
  Temporarily set a default Meerschaum repository for the duration of the shell.
835
907
  The default repository (mrsm.io) is loaded from the Meerschaum configuraton file
836
- (at keys 'meerschaum:default_repository').
908
+ (at keys 'meerschaum.repository').
837
909
 
838
910
  You can change the default repository with `edit config`.
839
911
 
@@ -864,7 +936,7 @@ class Shell(cmd.Cmd):
864
936
  except (IndexError, AttributeError):
865
937
  repo_keys = ''
866
938
  if repo_keys == '':
867
- repo_keys = get_config('meerschaum', 'default_repository', patch=True)
939
+ repo_keys = get_config('meerschaum', 'repository', patch=True)
868
940
 
869
941
  conn = parse_repo_keys(repo_keys, debug=debug)
870
942
  if conn is None or not conn:
@@ -947,7 +1019,7 @@ class Shell(cmd.Cmd):
947
1019
  show pipes -h
948
1020
  ```
949
1021
  """
950
- from meerschaum.actions import actions
1022
+ from meerschaum.actions import get_action
951
1023
  from meerschaum._internal.arguments._parser import parse_help
952
1024
  from meerschaum._internal.arguments._parse_arguments import parse_line
953
1025
  import textwrap
@@ -956,12 +1028,15 @@ class Shell(cmd.Cmd):
956
1028
  del args['action']
957
1029
  shell_attrs['_actions']['show'](['actions'], **args)
958
1030
  return ""
1031
+
959
1032
  if args['action'][0] not in shell_attrs['_actions']:
1033
+ action_func = get_action(args['action'])
960
1034
  try:
961
- print(textwrap.dedent(getattr(self, f"do_{args['action'][0]}").__doc__))
1035
+ print(textwrap.dedent(action_func.__doc__))
962
1036
  except Exception:
963
- print(f"No help on '{args['action'][0]}'.")
1037
+ print(f"No help on '{shlex.join(args['action'])}'.")
964
1038
  return ""
1039
+
965
1040
  parse_help(args)
966
1041
  return ""
967
1042
 
@@ -1045,9 +1120,10 @@ def input_with_sigint(_input, session, shell: Optional[Shell] = None):
1045
1120
  Replace built-in `input()` with prompt_toolkit.prompt.
1046
1121
  """
1047
1122
  from meerschaum.utils.formatting import CHARSET, ANSI, colored, UNICODE
1048
- from meerschaum.connectors import is_connected, connectors
1123
+ from meerschaum.connectors import connectors
1049
1124
  from meerschaum.utils.misc import remove_ansi, truncate_text_for_display
1050
1125
  from meerschaum.config import get_config
1126
+
1051
1127
  import platform
1052
1128
  if shell is None:
1053
1129
  from meerschaum.actions import get_shell
@@ -1062,8 +1138,16 @@ def input_with_sigint(_input, session, shell: Optional[Shell] = None):
1062
1138
  return None
1063
1139
  if not shell_attrs['_update_bottom_toolbar'] and platform.system() == 'Windows':
1064
1140
  return shell_attrs['_old_bottom_toolbar']
1065
- size = os.get_terminal_size()
1066
- num_cols, num_lines = size.columns, size.lines
1141
+ try:
1142
+ size = os.get_terminal_size()
1143
+ num_cols, num_lines = size.columns, size.lines
1144
+ except Exception:
1145
+ from meerschaum.utils.misc import is_int
1146
+ num_cols, num_lines = os.environ.get('COLUMNS', 80), os.environ.get('LINES', 120)
1147
+ if is_int(str(num_cols)):
1148
+ num_cols = int(num_cols)
1149
+ if is_int(str(num_lines)):
1150
+ num_lines = int(num_lines)
1067
1151
  truncation_suffix = (
1068
1152
  '…'
1069
1153
  if UNICODE
@@ -1112,24 +1196,34 @@ def input_with_sigint(_input, session, shell: Optional[Shell] = None):
1112
1196
 
1113
1197
  try:
1114
1198
  typ, label = shell_attrs['instance_keys'].split(':', maxsplit=1)
1115
- connected = typ in connectors and label in connectors[typ]
1199
+ cli_worker = (
1200
+ get_cli_daemon()
1201
+ if shell_attrs.get('daemon', False)
1202
+ else None
1203
+ )
1204
+ connected = (
1205
+ cli_worker.job.status == 'running'
1206
+ if cli_worker is not None
1207
+ else (typ in connectors and label in connectors[typ])
1208
+ )
1116
1209
  except Exception:
1117
1210
  connected = False
1118
1211
  last_connected = connected
1119
1212
  connected_str = truncate_text_for_display(
1120
- ('dis' if not connected else '') + 'connected',
1213
+ ('daemon' if cli_worker is not None else remove_ansi(shell_attrs['instance_keys'])),
1121
1214
  **truncation_kwargs
1122
1215
  )
1216
+ connected_status_str = 'disconnected' if not connected else 'connected'
1123
1217
  connected_icon = get_config(
1124
- 'formatting', connected_str, CHARSET, 'icon', warn=False,
1218
+ 'formatting', connected_status_str, CHARSET, 'icon', warn=False,
1125
1219
  ) or ''
1126
1220
  connection_text = (
1127
- connected_icon + ' ' + (
1128
- colored(connected_str.capitalize(), 'on ' + (get_config(
1129
- 'formatting', connected_str, 'ansi', 'rich', 'style',
1221
+ (
1222
+ colored(connected_str, 'on ' + (get_config(
1223
+ 'formatting', connected_status_str, 'ansi', 'rich', 'style',
1130
1224
  warn=False,
1131
- ) or '') + ' ') if ANSI else (colored(connected_str.capitalize(), 'on white') + ' ')
1132
- )
1225
+ ) or '') + ' ') if ANSI else (colored(connected_str, 'on white') + ' ')
1226
+ ) + ' ' + connected_icon
1133
1227
  )
1134
1228
 
1135
1229
  left = (
@@ -8,5 +8,8 @@ Import the Shell class definition
8
8
 
9
9
  from meerschaum._internal.shell.Shell import Shell
10
10
  from meerschaum._internal.shell.Shell import (
11
- default_action_completer, _completer_wrapper, _insert_shell_actions,
11
+ default_action_completer,
12
+ _completer_wrapper,
13
+ _insert_shell_actions,
14
+ _remove_shell_actions,
12
15
  )
@@ -240,6 +240,7 @@ STATIC_CONFIG: Dict[str, Any] = {
240
240
  'noask': 'MRSM_NOASK',
241
241
  'noninteractive': 'MRSM_NONINTERACTIVE',
242
242
  'id': 'MRSM_SERVER_ID',
243
+ 'test_flavors': 'MRSM_TEST_FLAVORS',
243
244
  'daemon_id': 'MRSM_DAEMON_ID',
244
245
  'systemd_log_path': 'MRSM_SYSTEMD_LOG_PATH',
245
246
  'systemd_stdin_path': 'MRSM_SYSTEMD_STDIN_PATH',
@@ -322,6 +323,8 @@ STATIC_CONFIG: Dict[str, Any] = {
322
323
  },
323
324
  'jobs': {
324
325
  'check_restart_seconds': 1.0,
326
+ 'stop_token': '<------- MRSM_STOP_TOKEN ------->',
327
+ 'clear_token': '<------- MRSM_CLEAR_TOKEN ------->',
325
328
  },
326
329
  'tokens': {
327
330
  'minimum_length': 24,
@@ -352,5 +355,4 @@ STATIC_CONFIG: Dict[str, Any] = {
352
355
  'users:delete': "Delete the associated user account (or other users for admins).",
353
356
  },
354
357
  },
355
-
356
358
  }
@@ -10,11 +10,10 @@ from __future__ import annotations
10
10
  from typing import Any
11
11
 
12
12
  import meerschaum as mrsm
13
- from meerschaum.utils.warnings import warn
14
13
  tornado_web = mrsm.attempt_import('tornado.web', lazy=False)
15
14
  terminado = mrsm.attempt_import('terminado', lazy=False)
16
15
 
17
- tmux_suffix = mrsm.get_config('system', 'webterm', 'tmux', 'session_suffix')
16
+ tmux_suffix = mrsm.get_config('api', 'webterm', 'tmux', 'session_suffix')
18
17
 
19
18
 
20
19
  class TermPageHandler(tornado_web.RequestHandler):
@@ -8,6 +8,9 @@ Build the web console virtual terminal using Tornado and xterm.js.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import os
12
+ import json
13
+ import pathlib
11
14
  from typing import Optional, Tuple
12
15
 
13
16
  import meerschaum as mrsm
@@ -16,7 +19,6 @@ from meerschaum._internal.term.TermPageHandler import TermPageHandler, CustomTer
16
19
  from meerschaum.config._paths import API_TEMPLATES_PATH, API_STATIC_PATH
17
20
  from meerschaum.utils.venv import venv_executable
18
21
  from meerschaum.utils.misc import is_tmux_available
19
- from meerschaum.utils.daemon._names import get_new_daemon_name
20
22
 
21
23
  tornado, tornado_ioloop, terminado = attempt_import(
22
24
  'tornado', 'tornado.ioloop', 'terminado', lazy=False,
@@ -25,6 +27,8 @@ tornado, tornado_ioloop, terminado = attempt_import(
25
27
 
26
28
  def get_webterm_app_and_manager(
27
29
  instance_keys: Optional[str] = None,
30
+ port: Optional[int] = None,
31
+ env_path: Optional[pathlib.Path] = None,
28
32
  ) -> Tuple[
29
33
  tornado.web.Application,
30
34
  terminado.UniqueTermManager,
@@ -36,19 +40,49 @@ def get_webterm_app_and_manager(
36
40
  -------
37
41
  A tuple of the Tornado web application and term manager.
38
42
  """
43
+ from meerschaum.config.environment import get_env_vars
44
+ from meerschaum._internal.static import STATIC_CONFIG
45
+ if env_path is None:
46
+ from meerschaum.config.paths import WEBTERM_INTERNAL_RESOURCES_PATH
47
+ env_path = WEBTERM_INTERNAL_RESOURCES_PATH / (str(port) + '.json')
48
+
49
+ env_vars = get_env_vars()
50
+ env_dict = {
51
+ env_var: os.environ[env_var]
52
+ for env_var in env_vars
53
+ if env_var not in (
54
+ STATIC_CONFIG['environment']['systemd_log_path'],
55
+ STATIC_CONFIG['environment']['systemd_result_path'],
56
+ STATIC_CONFIG['environment']['systemd_delete_job'],
57
+ STATIC_CONFIG['environment']['systemd_stdin_path'],
58
+ STATIC_CONFIG['environment']['daemon_id'],
59
+ )
60
+ }
61
+ with open(env_path, 'w+', encoding='utf-8') as f:
62
+ json.dump(env_dict, f)
63
+
39
64
  shell_kwargs_str = f"mrsm_instance='{instance_keys}'" if instance_keys else ""
40
65
  commands = [
41
66
  venv_executable(None),
42
67
  '-c',
43
- "import os; _ = os.environ.pop('COLUMNS', None); _ = os.environ.pop('LINES', None); "
44
- "from meerschaum._internal.entry import get_shell; "
68
+ "import os\n"
69
+ "import pathlib\n"
70
+ "import json\n"
71
+ f"env_path = pathlib.Path('{env_path.as_posix()}')\n"
72
+ "with open(env_path, 'r', encoding='utf-8') as f:\n"
73
+ " env_dict = json.load(f)\n"
74
+ "os.environ.update(env_dict)\n"
75
+ "_ = os.environ.pop('COLUMNS', None)\n"
76
+ "_ = os.environ.pop('LINES', None)\n"
77
+ "from meerschaum._internal.entry import get_shell\n"
45
78
  f"get_shell({shell_kwargs_str}).cmdloop()"
46
79
  ]
47
- webterm_cf = mrsm.get_config('system', 'webterm')
80
+ webterm_cf = mrsm.get_config('api', 'webterm')
48
81
  if webterm_cf.get('tmux', {}).get('enabled', False) and is_tmux_available():
49
- commands = ['tmux', 'new-session', '-A', '-s', 'MRSM_SESSION'] + commands
82
+ commands = ['tmux', 'new-session', '-A', '-s', f'MRSM_SESSION--{port}'] + commands
50
83
 
51
- term_manager = terminado.NamedTermManager(shell_command=commands)
84
+ term_manager = terminado.NamedTermManager(shell_command=commands, extra_env=env_dict)
85
+ term_manager._port = port
52
86
  handlers = [
53
87
  (
54
88
  r"/websocket/(.+)/?",
@@ -6,13 +6,14 @@
6
6
  Utility functions regarding the webterm.
7
7
  """
8
8
 
9
+ from typing import List, Optional
10
+
9
11
  import meerschaum as mrsm
10
- from meerschaum.utils.typing import List
11
12
 
12
13
 
13
14
  def is_webterm_running(
14
- host: str,
15
- port: int,
15
+ host: Optional[str] = None,
16
+ port: Optional[int] = None,
16
17
  protocol: str = 'http',
17
18
  session_id: str = 'mrsm',
18
19
  ) -> int:
@@ -20,10 +21,12 @@ def is_webterm_running(
20
21
  Determine whether the webterm service is running on a given host and port.
21
22
  """
22
23
  requests = mrsm.attempt_import('requests', lazy=False)
24
+ host = host or mrsm.get_config('api', 'webterm', 'host')
25
+ port = port or mrsm.get_config('api', 'webterm', 'port')
23
26
  url = f'{protocol}://{host}:{port}/webterm/{session_id}'
24
27
  try:
25
28
  r = requests.get(url, timeout=3)
26
- except Exception as e:
29
+ except Exception:
27
30
  return False
28
31
  if not r:
29
32
  return False
@@ -39,22 +42,44 @@ def kill_tmux_session(session: str) -> bool:
39
42
  return run_process(command, capture_output=True) == 0
40
43
 
41
44
 
42
- def get_mrsm_tmux_sessions() -> List[str]:
45
+ def get_mrsm_tmux_sessions(port: Optional[int] = None) -> List[str]:
43
46
  """
44
47
  Return a list of tmux sessions created by Meerschaum.
45
48
  """
46
49
  from meerschaum.utils.process import run_process
47
- tmux_suffix = mrsm.get_config('system', 'webterm', 'tmux', 'session_suffix')
50
+ tmux_suffix = mrsm.get_config('api', 'webterm', 'tmux', 'session_suffix')
48
51
  command = ['tmux', 'ls']
49
52
  proc = run_process(command, capture_output=True, as_proc=True)
50
53
  if proc.returncode != 0:
51
54
  return []
55
+
56
+ port = port or mrsm.get_config('api', 'webterm', 'port')
57
+
52
58
  sessions = [
53
59
  line.split(':', maxsplit=1)[0]
54
60
  for line in proc.stdout.read().decode('utf-8').split('\n')
55
61
  ]
62
+ mrsm_sessions_ports = []
63
+ for session in sessions:
64
+ if '--' not in session:
65
+ continue
66
+
67
+ parts = session.split('--', maxsplit=1)
68
+ if len(parts) != 2:
69
+ continue
70
+
71
+ if not parts[0].endswith(tmux_suffix):
72
+ continue
73
+
74
+ try:
75
+ session_port = int(parts[1])
76
+ except Exception:
77
+ continue
78
+
79
+ mrsm_sessions_ports.append((session, session_port))
80
+
56
81
  return [
57
82
  session
58
- for session in sessions
59
- if session.endswith(tmux_suffix)
83
+ for session, session_port in mrsm_sessions_ports
84
+ if session_port == port
60
85
  ]
@@ -9,7 +9,6 @@ Default actions available to the mrsm CLI.
9
9
  from __future__ import annotations
10
10
  from meerschaum.utils.typing import Callable, Any, Optional, Union, List, Dict, SuccessTuple
11
11
  from meerschaum.utils.packages import get_modules_from_package
12
- _custom_actions = []
13
12
 
14
13
  __all__ = (
15
14
  'get_action',
@@ -307,6 +306,8 @@ __all__ = ['actions', 'get_subactions', 'get_action', 'get_main_action_name', 'g
307
306
  ### functions that do not begin with '_' from all submodules.
308
307
  from inspect import getmembers, isfunction
309
308
  actions = {}
309
+ _custom_actions_plugins: Dict[str, str] = {}
310
+ _plugins_actions: Dict[str, List[str]] = {}
310
311
 
311
312
  for module in modules:
312
313
  ### A couple important things happening here:
@@ -329,8 +330,8 @@ for module in modules:
329
330
  if isfunction(ob[1])
330
331
  ### check that the function belongs to the module
331
332
  and ob[0] == module.__name__.replace('_', '').split('.')[-1]
332
- ### skip functions that start with '_'
333
- and ob[0][0] != '_'
333
+ ### skip functions that start with '__'
334
+ and ob[0][0] != '__'
334
335
  ]
335
336
  )
336
337
  )
@@ -362,4 +363,5 @@ __pdoc__ = {
362
363
  }
363
364
  for a in actions:
364
365
  __pdoc__[a] = False
365
- meerschaum.plugins.load_plugins()
366
+
367
+ meerschaum.plugins.load_plugins(skip_if_loaded=True)
meerschaum/actions/api.py CHANGED
@@ -6,7 +6,10 @@ Start the Meerschaum WebAPI with the `api` action.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+
9
10
  import os
11
+
12
+ import meerschaum as mrsm
10
13
  from meerschaum.utils.typing import SuccessTuple, Optional, List, Any
11
14
 
12
15
 
@@ -93,6 +96,7 @@ def _api_start(
93
96
  action: Optional[List[str]] = None,
94
97
  host: Optional[str] = None,
95
98
  port: Optional[int] = None,
99
+ webterm_port: Optional[int] = None,
96
100
  workers: Optional[int] = None,
97
101
  mrsm_instance: Optional[str] = None,
98
102
  no_dash: bool = False,
@@ -119,6 +123,10 @@ def _api_start(
119
123
  The address to bind to.
120
124
  If `None`, use '0.0.0.0'.
121
125
 
126
+ webterm_port: Optional[int], default None
127
+ Port to bind the webterm server to.
128
+ If `None`, use 8765.
129
+
122
130
  workers: Optional[int], default None
123
131
  How many worker threads to run.
124
132
  If `None`, defaults to the number of CPU cores or 1 on Android.
@@ -149,8 +157,6 @@ def _api_start(
149
157
 
150
158
  from meerschaum.utils.packages import (
151
159
  attempt_import,
152
- venv_contains_package,
153
- pip_install,
154
160
  run_python_package,
155
161
  )
156
162
  from meerschaum.utils.misc import is_int, filter_keywords
@@ -164,9 +170,10 @@ def _api_start(
164
170
  API_UVICORN_CONFIG_PATH,
165
171
  CACHE_RESOURCES_PATH,
166
172
  PACKAGE_ROOT_PATH,
173
+ ROOT_DIR_PATH,
167
174
  )
168
175
  from meerschaum.config._patch import apply_patch_to_config
169
- from meerschaum.config._environment import get_env_vars
176
+ from meerschaum.config.environment import get_env_vars
170
177
  from meerschaum._internal.static import STATIC_CONFIG, SERVER_ID
171
178
  from meerschaum.connectors.parse import parse_instance_keys
172
179
  from meerschaum.utils.pool import get_pool
@@ -187,9 +194,9 @@ def _api_start(
187
194
  uvicorn_config_path = API_UVICORN_RESOURCES_PATH / SERVER_ID / 'config.json'
188
195
  uvicorn_env_path = API_UVICORN_RESOURCES_PATH / SERVER_ID / 'uvicorn.env'
189
196
 
190
- api_config = deepcopy(get_config('system', 'api'))
197
+ api_config = deepcopy(get_config('api'))
191
198
  cf = _config()
192
- forwarded_allow_ips = get_config('system', 'api', 'uvicorn', 'forwarded_allow_ips')
199
+ forwarded_allow_ips = get_config('api', 'uvicorn', 'forwarded_allow_ips')
193
200
  uvicorn_config = api_config['uvicorn']
194
201
  if port is None:
195
202
  ### default
@@ -223,7 +230,7 @@ def _api_start(
223
230
  instance_connector = parse_instance_keys(mrsm_instance, debug=debug)
224
231
  if instance_connector.type == 'api' and instance_connector.protocol != 'https':
225
232
  allow_http_parent = get_config(
226
- 'system', 'api', 'permissions', 'chaining', 'insecure_parent_instance'
233
+ 'api', 'permissions', 'chaining', 'insecure_parent_instance'
227
234
  )
228
235
  if not allow_http_parent:
229
236
  return False, (
@@ -233,7 +240,7 @@ def _api_start(
233
240
  f" - Ensure that '{instance_connector}' is available over HTTPS, " +
234
241
  "and with `edit config`,\n" +
235
242
  f" change the `protocol` for '{instance_connector}' to 'https'.\n\n" +
236
- " - Run `edit config system` and search for `permissions`.\n" +
243
+ " - Run `edit config api` and search for `permissions`.\n" +
237
244
  " Under `api:permissions:chaining`, change the value of " +
238
245
  "`insecure_parent_instance` to `true`,\n" +
239
246
  " then restart the API process."
@@ -244,6 +251,7 @@ def _api_start(
244
251
  'host': host,
245
252
  'env_file': str(uvicorn_env_path.as_posix()),
246
253
  'mrsm_instance': mrsm_instance,
254
+ 'webterm_port': webterm_port,
247
255
  'no_dash': no_dash,
248
256
  'no_webterm': no_webterm or no_auth,
249
257
  'no_auth': no_auth,
@@ -261,9 +269,22 @@ def _api_start(
261
269
  uvicorn_config['use_colors'] = (not nopretty) if nopretty else ANSI
262
270
 
263
271
  api_config['uvicorn'] = uvicorn_config
264
- cf['system']['api']['uvicorn'] = uvicorn_config
272
+ cf['api']['uvicorn'] = uvicorn_config
265
273
  if secure:
266
- cf['system']['api']['permissions']['actions']['non_admin'] = False
274
+ cf['api']['permissions']['actions']['non_admin'] = False
275
+
276
+ if not uvicorn_config['no_webterm']:
277
+ from meerschaum._internal.term.tools import is_webterm_running
278
+ if webterm_port is None:
279
+ webterm_port = int(mrsm.get_config('api', 'webterm', 'port'))
280
+ if is_webterm_running(port=webterm_port):
281
+ return (
282
+ False,
283
+ (
284
+ f"Webterm is running on port {webterm_port}. "
285
+ "Start the API again with `--webterm-port`."
286
+ )
287
+ )
267
288
 
268
289
  custom_keys = [
269
290
  'mrsm_instance',
@@ -273,6 +294,7 @@ def _api_start(
273
294
  'private',
274
295
  'debug',
275
296
  'production',
297
+ 'webterm_port',
276
298
  ]
277
299
 
278
300
  ### write config to a temporary file to communicate with uvicorn threads
@@ -299,6 +321,7 @@ def _api_start(
299
321
  MRSM_SERVER_ID: SERVER_ID,
300
322
  MRSM_RUNTIME: 'api',
301
323
  MRSM_CONFIG: json.loads(os.environ.get(MRSM_CONFIG, '{}')),
324
+ MRSM_ROOT_DIR: ROOT_DIR_PATH.as_posix(),
302
325
  'FORWARDED_ALLOW_IPS': forwarded_allow_ips,
303
326
  'TERM': os.environ.get('TERM', 'screen-256color'),
304
327
  'SHELL': os.environ.get('SHELL', '/bin/bash'),
@@ -326,10 +349,10 @@ def _api_start(
326
349
  env_text = ''
327
350
  for key, val in env_dict.items():
328
351
  value = str(
329
- json.dumps(val, default=json_serialize_value)
352
+ json.dumps(val, default=json_serialize_value, separators=(',', ':'))
330
353
  if isinstance(val, (dict))
331
354
  else val
332
- ).replace('\\', '\\\\').replace("'", "'\\''")
355
+ ).replace('\\', '\\\\').replace("'", "\\'")
333
356
  env_text += f"{key}='{value}'\n"
334
357
  with open(uvicorn_env_path, 'w+', encoding='utf-8') as f:
335
358
  if debug:
@@ -394,11 +417,16 @@ def _api_start(
394
417
  except KeyboardInterrupt:
395
418
  pass
396
419
 
420
+ old_stdin = sys.stdin
421
+ sys.stdin = None
422
+
397
423
  if production:
398
424
  _run_gunicorn()
399
425
  else:
400
426
  _run_uvicorn()
401
427
 
428
+ sys.stdin = old_stdin
429
+
402
430
  ### Cleanup
403
431
  if uvicorn_config_path.parent.exists():
404
432
  shutil.rmtree(uvicorn_config_path.parent)
@@ -9,6 +9,7 @@ Attach to running jobs.
9
9
  import meerschaum as mrsm
10
10
  from meerschaum.utils.typing import Optional, List, Any, SuccessTuple
11
11
 
12
+
12
13
  def attach(
13
14
  action: Optional[List[str]] = None,
14
15
  executor_keys: Optional[str] = None,