meerschaum 2.3.0rc2__py3-none-any.whl → 2.3.0rc5__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 (34) hide show
  1. meerschaum/_internal/arguments/__init__.py +1 -1
  2. meerschaum/_internal/arguments/_parse_arguments.py +17 -12
  3. meerschaum/_internal/arguments/_parser.py +3 -6
  4. meerschaum/_internal/entry.py +42 -4
  5. meerschaum/_internal/shell/Shell.py +84 -72
  6. meerschaum/actions/delete.py +4 -0
  7. meerschaum/actions/show.py +5 -5
  8. meerschaum/actions/start.py +71 -1
  9. meerschaum/api/dash/callbacks/jobs.py +36 -44
  10. meerschaum/api/dash/jobs.py +24 -15
  11. meerschaum/api/routes/_actions.py +54 -6
  12. meerschaum/api/routes/_jobs.py +19 -1
  13. meerschaum/config/_jobs.py +1 -1
  14. meerschaum/config/_paths.py +1 -0
  15. meerschaum/config/_version.py +1 -1
  16. meerschaum/config/static/__init__.py +2 -0
  17. meerschaum/connectors/api/APIConnector.py +7 -1
  18. meerschaum/connectors/api/_actions.py +77 -1
  19. meerschaum/connectors/api/_jobs.py +13 -2
  20. meerschaum/connectors/api/_pipes.py +85 -84
  21. meerschaum/jobs/_Job.py +53 -12
  22. meerschaum/jobs/_SystemdExecutor.py +111 -30
  23. meerschaum/jobs/__init__.py +9 -2
  24. meerschaum/utils/daemon/Daemon.py +14 -0
  25. meerschaum/utils/daemon/StdinFile.py +1 -0
  26. meerschaum/utils/daemon/_names.py +15 -13
  27. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/METADATA +1 -1
  28. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/RECORD +34 -34
  29. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/LICENSE +0 -0
  30. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/NOTICE +0 -0
  31. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/WHEEL +0 -0
  32. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/entry_points.txt +0 -0
  33. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/top_level.txt +0 -0
  34. {meerschaum-2.3.0rc2.dist-info → meerschaum-2.3.0rc5.dist-info}/zip-safe +0 -0
@@ -8,7 +8,7 @@ This package includes argument parsing utilities.
8
8
 
9
9
  from meerschaum._internal.arguments._parse_arguments import (
10
10
  parse_arguments, parse_line, remove_leading_action,
11
- parse_dict_to_sysargs, split_chained_sysargs,
11
+ parse_dict_to_sysargs, split_chained_sysargs, split_pipeline_sysargs,
12
12
  )
13
13
  from meerschaum._internal.arguments._parser import parser
14
14
  from meerschaum.plugins import add_plugin_argument
@@ -10,7 +10,7 @@ This module contains functions for parsing arguments
10
10
  from __future__ import annotations
11
11
  import json
12
12
  from datetime import timedelta
13
- from meerschaum.utils.typing import List, Dict, Any, Optional, Callable, SuccessTuple
13
+ from meerschaum.utils.typing import List, Dict, Any, Optional, Callable, SuccessTuple, Tuple
14
14
  from meerschaum.utils.threading import RLock
15
15
 
16
16
  _locks = {
@@ -18,10 +18,25 @@ _locks = {
18
18
  }
19
19
  _loaded_plugins_args: bool = False
20
20
 
21
+ def split_pipeline_sysargs(sysargs: List[str]) -> Tuple[List[str], List[str]]:
22
+ """
23
+ Split `sysargs` into the main pipeline and the flags following the pipeline separator (`:`).
24
+ """
25
+ from meerschaum.config.static import STATIC_CONFIG
26
+ pipeline_key = STATIC_CONFIG['system']['arguments']['pipeline_key']
27
+ if pipeline_key not in sysargs:
28
+ return sysargs, []
29
+
30
+ ### Find the index of the last occurrence of `:`.
31
+ pipeline_ix = len(sysargs) - 1 - sysargs[::-1].index(pipeline_key)
32
+ sysargs_after_pipeline_key = sysargs[pipeline_ix+1:]
33
+ sysargs = [arg for arg in sysargs[:pipeline_ix] if arg != pipeline_key]
34
+ return sysargs, sysargs_after_pipeline_key
35
+
21
36
 
22
37
  def split_chained_sysargs(sysargs: List[str]) -> List[List[str]]:
23
38
  """
24
- Split a `sysargs` list containing "and" keys (`&&`)
39
+ Split a `sysargs` list containing "and" keys (`+`)
25
40
  into a list of individual `sysargs`.
26
41
  """
27
42
  from meerschaum.config.static import STATIC_CONFIG
@@ -249,17 +264,7 @@ def parse_dict_to_sysargs(
249
264
  from meerschaum.config.static import STATIC_CONFIG
250
265
  from meerschaum.utils.warnings import warn
251
266
 
252
- and_key = STATIC_CONFIG['system']['arguments']['and_key']
253
- if (line := args_dict.get('line', None)):
254
- return shlex.split(line)
255
-
256
- if (_sysargs := args_dict.get('sysargs', None)):
257
- return _sysargs
258
-
259
267
  action = args_dict.get('action', None)
260
- if action and and_key in action:
261
- warn(f"Cannot determine flags from chained actions:\n{args_dict}")
262
-
263
268
  sysargs: List[str] = []
264
269
  sysargs.extend(action or [])
265
270
  allow_none_args = {'location_keys'}
@@ -215,13 +215,10 @@ groups['jobs'].add_argument(
215
215
  help=("Restart a job if not stopped manually."),
216
216
  )
217
217
  groups['jobs'].add_argument(
218
- '--systemd', action='store_true',
219
- help=("Create a job via systemd. Shorthand for `-e systemd`."),
220
- )
221
- groups['jobs'].add_argument(
222
- '--executor-keys', '--executor', '-e', type=parse_executor_keys,
218
+ '-e', '--executor-keys', type=parse_executor_keys,
223
219
  help=(
224
- "Execute jobs on an API instance or via systemd."
220
+ "Execute jobs locally or remotely. "
221
+ "Supported values are 'local', 'systemd', and 'api:{label}'."
225
222
  ),
226
223
  )
227
224
  groups['jobs'].add_argument(
@@ -32,7 +32,10 @@ if (_STATIC_CONFIG['environment']['systemd_log_path']) in os.environ:
32
32
  if _systemd_stdin_path:
33
33
  sys.stdin = _StdinFile(_systemd_stdin_path)
34
34
 
35
- def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
35
+ def entry(
36
+ sysargs: Optional[List[str]] = None,
37
+ _patch_args: Optional[Dict[str, Any]] = None,
38
+ ) -> SuccessTuple:
36
39
  """
37
40
  Parse arguments and launch a Meerschaum action.
38
41
 
@@ -41,14 +44,23 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
41
44
  A `SuccessTuple` indicating success.
42
45
  """
43
46
  import shlex
47
+ import json
44
48
  from meerschaum.utils.formatting import make_header
45
- from meerschaum._internal.arguments import parse_arguments, split_chained_sysargs
49
+ from meerschaum._internal.arguments import (
50
+ parse_arguments,
51
+ split_chained_sysargs,
52
+ split_pipeline_sysargs,
53
+ )
46
54
  from meerschaum.config.static import STATIC_CONFIG
47
55
  if sysargs is None:
48
56
  sysargs = []
49
57
  if not isinstance(sysargs, list):
50
58
  sysargs = shlex.split(sysargs)
51
59
 
60
+ pipeline_key = STATIC_CONFIG['system']['arguments']['pipeline_key']
61
+ escaped_pipeline_key = STATIC_CONFIG['system']['arguments']['escaped_pipeline_key']
62
+ sysargs, pipeline_args = split_pipeline_sysargs(sysargs)
63
+
52
64
  has_daemon = '-d' in sysargs or '--daemon' in sysargs
53
65
  has_start_job = sysargs[:2] == ['start', 'job']
54
66
  chained_sysargs = (
@@ -56,10 +68,32 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
56
68
  if has_daemon or has_start_job
57
69
  else split_chained_sysargs(sysargs)
58
70
  )
71
+ if pipeline_args:
72
+ chained_sysargs = [
73
+ ['start', 'pipeline']
74
+ + [str(arg) for arg in pipeline_args]
75
+ + (
76
+ ['--params', json.dumps(_patch_args)]
77
+ if _patch_args
78
+ else []
79
+ )
80
+ + ['--sub-args', shlex.join(sysargs)]
81
+ ]
82
+
59
83
  results: List[SuccessTuple] = []
60
84
 
61
85
  for _sysargs in chained_sysargs:
86
+ if escaped_pipeline_key in _sysargs:
87
+ _sysargs = [
88
+ pipeline_key
89
+ if _arg == escaped_pipeline_key
90
+ else _arg
91
+ for _arg in _sysargs
92
+ ]
93
+
62
94
  args = parse_arguments(_sysargs)
95
+ if _patch_args:
96
+ args.update(_patch_args)
63
97
  argparse_exception = args.get(
64
98
  STATIC_CONFIG['system']['arguments']['failure_key'],
65
99
  None,
@@ -76,7 +110,7 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
76
110
  )
77
111
  )
78
112
 
79
- entry_success, entry_msg = entry_with_args(**args)
113
+ entry_success, entry_msg = entry_with_args(_patch_args=_patch_args, **args)
80
114
  if not entry_success:
81
115
  return entry_success, entry_msg
82
116
 
@@ -106,6 +140,7 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
106
140
 
107
141
  def entry_with_args(
108
142
  _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
143
+ _patch_args: Optional[Dict[str, Any]] = None,
109
144
  **kw
110
145
  ) -> SuccessTuple:
111
146
  """Execute a Meerschaum action with keyword arguments.
@@ -119,12 +154,15 @@ def entry_with_args(
119
154
  from meerschaum.utils.venv import active_venvs, deactivate_venv
120
155
  from meerschaum.config.static import STATIC_CONFIG
121
156
 
157
+ if _patch_args:
158
+ kw.update(_patch_args)
159
+
122
160
  and_key = STATIC_CONFIG['system']['arguments']['and_key']
123
161
  escaped_and_key = STATIC_CONFIG['system']['arguments']['escaped_and_key']
124
162
  if and_key in (sysargs := kw.get('sysargs', [])):
125
163
  if '-d' in sysargs or '--daemon' in sysargs:
126
164
  sysargs = [(arg if arg != and_key else escaped_and_key) for arg in sysargs]
127
- return entry(sysargs)
165
+ return entry(sysargs, _patch_args=_patch_args)
128
166
 
129
167
  if kw.get('trace', None):
130
168
  from meerschaum.utils.misc import debug_trace
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
  import os
10
10
  from copy import deepcopy
11
11
  from itertools import chain
12
+ import shlex
12
13
 
13
14
  from meerschaum.utils.typing import Union, SuccessTuple, Any, Callable, Optional, List, Dict
14
15
  from meerschaum.utils.packages import attempt_import
@@ -31,12 +32,15 @@ from meerschaum._internal.shell.ValidAutoSuggest import ValidAutoSuggest
31
32
  from meerschaum._internal.shell.ShellCompleter import ShellCompleter
32
33
  _clear_screen = get_config('shell', 'clear_screen', patch=True)
33
34
  from meerschaum.utils.misc import string_width, remove_ansi
35
+ from meerschaum.utils.warnings import warn
34
36
  from meerschaum.jobs import get_executor_keys_from_context
35
37
  from meerschaum.config.static import STATIC_CONFIG
36
38
  from meerschaum._internal.arguments._parse_arguments import (
37
39
  split_chained_sysargs,
40
+ split_pipeline_sysargs,
38
41
  parse_arguments,
39
42
  parse_line,
43
+ parse_dict_to_sysargs,
40
44
  )
41
45
 
42
46
  patch = True
@@ -73,6 +77,8 @@ reserved_completers = {
73
77
  shell_attrs = {}
74
78
  AND_KEY: str = STATIC_CONFIG['system']['arguments']['and_key']
75
79
  ESCAPED_AND_KEY: str = STATIC_CONFIG['system']['arguments']['escaped_and_key']
80
+ PIPELINE_KEY: str = STATIC_CONFIG['system']['arguments']['pipeline_key']
81
+ ESCAPED_PIPELINE_KEY: str = STATIC_CONFIG['system']['arguments']['escaped_pipeline_key']
76
82
 
77
83
  def _insert_shell_actions(
78
84
  _shell: Optional['Shell'] = None,
@@ -173,7 +179,6 @@ def _check_complete_keys(line: str) -> Optional[List[str]]:
173
179
 
174
180
  ### TODO Find out arg possibilities
175
181
  possibilities = []
176
- # last_word = line.split(' ')[-1]
177
182
  last_word = line.rstrip(' ').split(' ')[-1]
178
183
 
179
184
  if last_word.startswith('-'):
@@ -485,6 +490,9 @@ class Shell(cmd.Cmd):
485
490
  ### make a backup of line for later
486
491
  original_line = deepcopy(line)
487
492
 
493
+ ### Escape backslashes to allow for multi-line input.
494
+ line = line.replace('\\\n', ' ')
495
+
488
496
  ### cmd2 support: check if command exists
489
497
  try:
490
498
  command = line.command
@@ -515,48 +523,71 @@ class Shell(cmd.Cmd):
515
523
  return "help " + line[len(help_token):]
516
524
 
517
525
  from meerschaum._internal.arguments import parse_line
518
- args = parse_line(line)
519
- if args.get('help', False):
526
+ try:
527
+ sysargs = shlex.split(line)
528
+ except ValueError as e:
529
+ warn(e, stack=False)
530
+ return ""
531
+
532
+ sysargs, pipeline_args = split_pipeline_sysargs(sysargs)
533
+ chained_sysargs = split_chained_sysargs(sysargs)
534
+ chained_kwargs = [
535
+ parse_arguments(_sysargs)
536
+ for _sysargs in chained_sysargs
537
+ ]
538
+
539
+ if '--help' in sysargs or '-h' in sysargs:
520
540
  from meerschaum._internal.arguments._parser import parse_help
521
- parse_help(args)
541
+ parse_help(sysargs)
522
542
  return ""
523
543
 
544
+ patch_args: Dict[str, Any] = {}
545
+
524
546
  ### NOTE: pass `shell` flag in case actions need to distinguish between
525
547
  ### being run on the command line and being run in the shell
526
- args['shell'] = True
527
- args['line'] = line
548
+ patch_args.update({
549
+ 'shell': True,
550
+ 'line': line,
551
+ })
552
+ patches: List[Dict[str, Any]] = [{} for _ in chained_kwargs]
528
553
 
529
554
  ### if debug is not set on the command line,
530
555
  ### default to shell setting
531
- if not args.get('debug', False):
532
- args['debug'] = shell_attrs['debug']
556
+ for kwargs in chained_kwargs:
557
+ if not kwargs.get('debug', False):
558
+ kwargs['debug'] = shell_attrs['debug']
533
559
 
534
560
  ### Make sure an action was provided.
535
- if not args.get('action', None):
561
+ if (
562
+ not chained_kwargs
563
+ or not chained_kwargs[0].get('action', None)
564
+ ):
536
565
  self.emptyline()
537
566
  return ''
538
567
 
539
568
  ### Strip a leading 'mrsm' if it's provided.
540
- if args['action'][0] == 'mrsm':
541
- args['action'] = args['action'][1:]
542
- if not args['action']:
543
- self.emptyline()
544
- return ''
569
+ for kwargs in chained_kwargs:
570
+ if kwargs['action'][0] == 'mrsm':
571
+ kwargs['action'] = kwargs['action'][1:]
572
+ if not kwargs['action']:
573
+ self.emptyline()
574
+ return ''
545
575
 
546
576
  ### If we don't recognize the action,
547
577
  ### make it a shell action.
548
- from meerschaum.actions import get_main_action_name
549
- main_action_name = get_main_action_name(args['action'])
550
- if main_action_name is None:
551
- if not hasattr(self, 'do_' + args['action'][0]):
552
- args['action'].insert(0, 'sh')
553
- main_action_name = 'sh'
554
- else:
555
- main_action_name = args['action'][0]
556
-
557
- def _add_flag_to_sysargs(
558
- chained_sysargs, key, value, flag, shell_key=None,
559
- ):
578
+ ### TODO: make this work for chained actions
579
+ def _get_main_action_name(kwargs):
580
+ from meerschaum.actions import get_main_action_name
581
+ main_action_name = get_main_action_name(kwargs['action'])
582
+ if main_action_name is None:
583
+ if not hasattr(self, 'do_' + kwargs['action'][0]):
584
+ kwargs['action'].insert(0, 'sh')
585
+ main_action_name = 'sh'
586
+ else:
587
+ main_action_name = kwargs['action'][0]
588
+ return main_action_name
589
+
590
+ def _add_flag_to_kwargs(kwargs, i, key, shell_key=None):
560
591
  shell_key = shell_key or key
561
592
  shell_value = remove_ansi(shell_attrs.get(shell_key) or '')
562
593
  if key == 'mrsm_instance':
@@ -568,68 +599,49 @@ class Shell(cmd.Cmd):
568
599
  else:
569
600
  default_value = None
570
601
 
571
- if shell_value == default_value:
602
+ if key in kwargs or shell_value == default_value:
572
603
  return
573
604
 
574
- for sysargs in chained_sysargs:
575
- kwargs = parse_arguments(sysargs)
576
- if key in kwargs:
577
- continue
578
- sysargs.extend([flag, value])
579
-
580
- args['sysargs'] = list(
581
- chain.from_iterable(
582
- sub_sysargs + [AND_KEY]
583
- for sub_sysargs in chained_sysargs
584
- )
585
- )[:-1]
586
-
605
+ patches[i][key] = shell_value
587
606
 
588
607
  ### if no instance is provided, use current shell default,
589
608
  ### but not for the 'api' command (to avoid recursion)
590
- if main_action_name != 'api':
591
- chained_sysargs = split_chained_sysargs(args['sysargs'])
592
- chained_filtered_sysargs = split_chained_sysargs(args['filtered_sysargs'])
593
- mrsm_instance = shell_attrs['instance_keys']
594
- args['mrsm_instance'] = mrsm_instance
595
- _add_flag_to_sysargs(
596
- chained_sysargs, 'mrsm_instance', mrsm_instance, '-i', 'instance_keys',
597
- )
598
- _add_flag_to_sysargs(
599
- chained_filtered_sysargs, 'mrsm_instance', mrsm_instance, '-i', 'instance_keys',
600
- )
601
-
602
- repo_keys = str(shell_attrs['repo_keys'])
603
- args['repository'] = repo_keys
604
- _add_flag_to_sysargs(
605
- chained_sysargs, 'repository', repo_keys, '-r', 'repo_keys',
606
- )
607
- _add_flag_to_sysargs(
608
- chained_filtered_sysargs, 'repository', repo_keys, '-r', 'repo_keys',
609
- )
609
+ for i, kwargs in enumerate(chained_kwargs):
610
+ main_action_name = _get_main_action_name(kwargs)
611
+ if main_action_name == 'api':
612
+ continue
610
613
 
611
- executor_keys = remove_ansi(str(shell_attrs['executor_keys']))
612
- args['executor_keys'] = remove_ansi(executor_keys)
613
- _add_flag_to_sysargs(
614
- chained_sysargs, 'executor_keys', executor_keys, '-e',
615
- )
616
- _add_flag_to_sysargs(
617
- chained_filtered_sysargs, 'executor_keys', executor_keys, '-e',
618
- )
614
+ _add_flag_to_kwargs(kwargs, i, 'mrsm_instance', shell_key='instance_keys')
615
+ _add_flag_to_kwargs(kwargs, i, 'repository', shell_key='repo_keys')
616
+ _add_flag_to_kwargs(kwargs, i, 'executor_keys')
619
617
 
620
618
  ### parse out empty strings
621
- if args['action'][0].strip("\"'") == '':
619
+ if chained_kwargs[0]['action'][0].strip("\"'") == '':
622
620
  self.emptyline()
623
621
  return ""
624
622
 
625
- positional_only = (main_action_name not in shell_attrs['_actions'])
623
+ positional_only = (_get_main_action_name(chained_kwargs[0]) not in shell_attrs['_actions'])
626
624
  if positional_only:
627
625
  return original_line
628
626
 
629
- from meerschaum._internal.entry import entry_with_args
627
+ ### Apply patch to all kwargs.
628
+ for i, kwargs in enumerate([_ for _ in chained_kwargs]):
629
+ kwargs.update(patches[i])
630
+
631
+ from meerschaum._internal.entry import entry_with_args, entry
632
+ sysargs_to_execute = []
633
+ for i, kwargs in enumerate(chained_kwargs):
634
+ step_kwargs = {k: v for k, v in kwargs.items() if k != 'line'}
635
+ step_sysargs = parse_dict_to_sysargs(step_kwargs)
636
+ sysargs_to_execute.extend(step_sysargs)
637
+ sysargs_to_execute.append(AND_KEY)
638
+
639
+ sysargs_to_execute = sysargs_to_execute[:-1] + (
640
+ ([':'] + pipeline_args) if pipeline_args else []
641
+ )
630
642
 
631
643
  try:
632
- success_tuple = entry_with_args(_actions=shell_attrs['_actions'], **args)
644
+ success_tuple = entry(sysargs_to_execute, _patch_args=patch_args)
633
645
  except Exception as e:
634
646
  success_tuple = False, str(e)
635
647
 
@@ -512,6 +512,7 @@ def _complete_delete_jobs(
512
512
  get_executor_keys_from_context,
513
513
  )
514
514
  from meerschaum.utils.misc import remove_ansi
515
+ from meerschaum.connectors.parse import parse_executor_keys
515
516
 
516
517
  executor_keys = (
517
518
  executor_keys
@@ -520,6 +521,9 @@ def _complete_delete_jobs(
520
521
  )
521
522
  )
522
523
 
524
+ if parse_executor_keys(executor_keys, construct=False) is None:
525
+ return []
526
+
523
527
  jobs = get_jobs(executor_keys, include_hidden=False)
524
528
  if _get_job_method:
525
529
  method_keys = [_get_job_method] if isinstance(_get_job_method, str) else _get_job_method
@@ -155,10 +155,10 @@ def _complete_show_config(action: Optional[List[str]] = None, **kw : Any):
155
155
 
156
156
 
157
157
  def _show_pipes(
158
- nopretty: bool = False,
159
- debug: bool = False,
160
- **kw: Any
161
- ) -> SuccessTuple:
158
+ nopretty: bool = False,
159
+ debug: bool = False,
160
+ **kw: Any
161
+ ) -> SuccessTuple:
162
162
  """
163
163
  Print a stylized tree of available Meerschaum pipes.
164
164
  Respects global ANSI and UNICODE settings.
@@ -170,7 +170,7 @@ def _show_pipes(
170
170
  pipes = get_pipes(debug=debug, **kw)
171
171
 
172
172
  if len(pipes) == 0:
173
- return False, "No pipes to show."
173
+ return True, "No pipes to show."
174
174
 
175
175
  if len(flatten_pipes_dict(pipes)) == 1:
176
176
  return flatten_pipes_dict(pipes)[0].show(debug=debug, nopretty=nopretty, **kw)
@@ -7,7 +7,7 @@ Start subsystems (API server, logging daemon, etc.).
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- from meerschaum.utils.typing import SuccessTuple, Optional, List, Any
10
+ from meerschaum.utils.typing import SuccessTuple, Optional, List, Any, Union
11
11
 
12
12
  def start(
13
13
  action: Optional[List[str]] = None,
@@ -23,6 +23,7 @@ def start(
23
23
  'gui': _start_gui,
24
24
  'webterm': _start_webterm,
25
25
  'connectors': _start_connectors,
26
+ 'pipeline': _start_pipeline,
26
27
  }
27
28
  return choose_subaction(action, options, **kw)
28
29
 
@@ -537,6 +538,75 @@ def _complete_start_connectors(**kw) -> List[str]:
537
538
  return _complete_show_connectors(**kw)
538
539
 
539
540
 
541
+ def _start_pipeline(
542
+ action: Optional[List[str]] = None,
543
+ sub_args: Optional[List[str]] = None,
544
+ loop: bool = False,
545
+ min_seconds: Union[float, int, None] = 1.0,
546
+ params: Optional[Dict[str, Any]] = None,
547
+ **kwargs
548
+ ) -> SuccessTuple:
549
+ """
550
+ Run a series of Meerschaum commands as a single action.
551
+
552
+ Add `:` to the end of chained arguments to apply additional flags to the pipeline.
553
+
554
+ Examples
555
+ --------
556
+
557
+ `sync pipes -i sql:local + sync pipes -i sql:main :: -s 'daily'`
558
+
559
+ `show version + show arguments :: --loop`
560
+
561
+ """
562
+ import time
563
+ from meerschaum._internal.entry import entry
564
+ from meerschaum.utils.warnings import info, warn
565
+ from meerschaum.utils.misc import is_int
566
+
567
+ do_n_times = (
568
+ int(action[0].lstrip('x'))
569
+ if action and is_int(action[0].lstrip('x'))
570
+ else 1
571
+ )
572
+
573
+ if not sub_args:
574
+ return False, "Nothing to do."
575
+
576
+ if min_seconds is None:
577
+ min_seconds = 1.0
578
+
579
+ ran_n_times = 0
580
+ success, msg = False, "Did not run pipeline."
581
+ def run_loop():
582
+ nonlocal ran_n_times, success, msg
583
+ while True:
584
+ success, msg = entry(sub_args, _patch_args=params)
585
+ ran_n_times += 1
586
+
587
+ if not loop and do_n_times == 1:
588
+ break
589
+
590
+ if min_seconds != 0 and ran_n_times != do_n_times:
591
+ info(f"Sleeping for {min_seconds} seconds...")
592
+ time.sleep(min_seconds)
593
+
594
+ if loop:
595
+ continue
596
+
597
+ if ran_n_times >= do_n_times:
598
+ break
599
+
600
+ try:
601
+ run_loop()
602
+ except KeyboardInterrupt:
603
+ warn("Cancelled pipeline.", stack=False)
604
+
605
+ if do_n_times != 1:
606
+ info(f"Ran pipeline {ran_n_times} time" + ('s' if ran_n_times != 1 else '') + '.')
607
+ return success, msg
608
+
609
+
540
610
  ### NOTE: This must be the final statement of the module.
541
611
  ### Any subactions added below these lines will not
542
612
  ### be added to the `help` docstring.