meerschaum 2.2.6__py3-none-any.whl → 2.3.0__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 (80) hide show
  1. meerschaum/__init__.py +6 -1
  2. meerschaum/__main__.py +9 -9
  3. meerschaum/_internal/arguments/__init__.py +1 -1
  4. meerschaum/_internal/arguments/_parse_arguments.py +72 -6
  5. meerschaum/_internal/arguments/_parser.py +45 -15
  6. meerschaum/_internal/docs/index.py +265 -8
  7. meerschaum/_internal/entry.py +167 -37
  8. meerschaum/_internal/shell/Shell.py +290 -99
  9. meerschaum/_internal/shell/updates.py +175 -0
  10. meerschaum/actions/__init__.py +29 -17
  11. meerschaum/actions/api.py +12 -12
  12. meerschaum/actions/attach.py +113 -0
  13. meerschaum/actions/copy.py +68 -41
  14. meerschaum/actions/delete.py +112 -50
  15. meerschaum/actions/edit.py +3 -3
  16. meerschaum/actions/install.py +40 -32
  17. meerschaum/actions/pause.py +44 -27
  18. meerschaum/actions/register.py +19 -5
  19. meerschaum/actions/restart.py +107 -0
  20. meerschaum/actions/show.py +130 -159
  21. meerschaum/actions/start.py +161 -100
  22. meerschaum/actions/stop.py +78 -42
  23. meerschaum/actions/sync.py +3 -3
  24. meerschaum/actions/upgrade.py +28 -36
  25. meerschaum/api/_events.py +25 -1
  26. meerschaum/api/_oauth2.py +2 -0
  27. meerschaum/api/_websockets.py +2 -2
  28. meerschaum/api/dash/callbacks/jobs.py +36 -44
  29. meerschaum/api/dash/jobs.py +89 -78
  30. meerschaum/api/routes/__init__.py +1 -0
  31. meerschaum/api/routes/_actions.py +148 -17
  32. meerschaum/api/routes/_jobs.py +407 -0
  33. meerschaum/api/routes/_pipes.py +25 -25
  34. meerschaum/config/_default.py +1 -0
  35. meerschaum/config/_formatting.py +1 -0
  36. meerschaum/config/_jobs.py +1 -1
  37. meerschaum/config/_paths.py +11 -0
  38. meerschaum/config/_shell.py +84 -67
  39. meerschaum/config/_version.py +1 -1
  40. meerschaum/config/static/__init__.py +18 -0
  41. meerschaum/connectors/Connector.py +13 -7
  42. meerschaum/connectors/__init__.py +28 -15
  43. meerschaum/connectors/api/APIConnector.py +27 -1
  44. meerschaum/connectors/api/_actions.py +71 -6
  45. meerschaum/connectors/api/_jobs.py +368 -0
  46. meerschaum/connectors/api/_misc.py +1 -1
  47. meerschaum/connectors/api/_pipes.py +85 -84
  48. meerschaum/connectors/api/_request.py +13 -9
  49. meerschaum/connectors/parse.py +27 -15
  50. meerschaum/core/Pipe/_bootstrap.py +16 -8
  51. meerschaum/core/Pipe/_sync.py +3 -0
  52. meerschaum/jobs/_Executor.py +69 -0
  53. meerschaum/jobs/_Job.py +899 -0
  54. meerschaum/jobs/__init__.py +396 -0
  55. meerschaum/jobs/systemd.py +694 -0
  56. meerschaum/plugins/__init__.py +97 -12
  57. meerschaum/utils/daemon/Daemon.py +352 -147
  58. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  59. meerschaum/utils/daemon/RotatingFile.py +22 -8
  60. meerschaum/utils/daemon/StdinFile.py +121 -0
  61. meerschaum/utils/daemon/__init__.py +42 -27
  62. meerschaum/utils/daemon/_names.py +15 -13
  63. meerschaum/utils/formatting/__init__.py +83 -37
  64. meerschaum/utils/formatting/_jobs.py +146 -55
  65. meerschaum/utils/formatting/_shell.py +6 -0
  66. meerschaum/utils/misc.py +41 -22
  67. meerschaum/utils/packages/__init__.py +21 -15
  68. meerschaum/utils/packages/_packages.py +9 -6
  69. meerschaum/utils/process.py +9 -9
  70. meerschaum/utils/prompt.py +20 -7
  71. meerschaum/utils/schedule.py +21 -15
  72. meerschaum/utils/venv/__init__.py +2 -2
  73. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/METADATA +22 -25
  74. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/RECORD +80 -70
  75. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/WHEEL +1 -1
  76. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/LICENSE +0 -0
  77. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/NOTICE +0 -0
  78. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/entry_points.txt +0 -0
  79. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/top_level.txt +0 -0
  80. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dist-info}/zip-safe +0 -0
meerschaum/__init__.py CHANGED
@@ -20,15 +20,17 @@ limitations under the License.
20
20
 
21
21
  import atexit
22
22
  from meerschaum.utils.typing import SuccessTuple
23
+ from meerschaum.utils.packages import attempt_import
23
24
  from meerschaum.core.Pipe import Pipe
24
25
  from meerschaum.plugins import Plugin
25
26
  from meerschaum.utils.venv import Venv
27
+ from meerschaum.jobs import Job, make_executor
26
28
  from meerschaum.connectors import get_connector, Connector, make_connector
27
29
  from meerschaum.utils import get_pipes
28
30
  from meerschaum.utils.formatting import pprint
29
31
  from meerschaum._internal.docs import index as __doc__
30
32
  from meerschaum.config import __version__, get_config
31
- from meerschaum.utils.packages import attempt_import
33
+ from meerschaum._internal.entry import entry
32
34
  from meerschaum.__main__ import _close_pools
33
35
 
34
36
  atexit.register(_close_pools)
@@ -42,14 +44,17 @@ __all__ = (
42
44
  "Plugin",
43
45
  "Venv",
44
46
  "Plugin",
47
+ "Job",
45
48
  "pprint",
46
49
  "attempt_import",
47
50
  "actions",
48
51
  "config",
49
52
  "connectors",
53
+ "jobs",
50
54
  "plugins",
51
55
  "utils",
52
56
  "SuccessTuple",
53
57
  "Connector",
54
58
  "make_connector",
59
+ "entry",
55
60
  )
meerschaum/__main__.py CHANGED
@@ -19,9 +19,15 @@ See the License for the specific language governing permissions and
19
19
  limitations under the License.
20
20
  """
21
21
 
22
- import sys, os, copy
22
+ import sys
23
+ import os
24
+ import copy
23
25
 
24
- def main(sysargs: list = None) -> None:
26
+ from meerschaum.utils.typing import List, Optional
27
+ from meerschaum.utils.formatting import print_tuple as _print_tuple
28
+
29
+
30
+ def main(sysargs: Optional[List[str]] = None) -> None:
25
31
  """Main CLI entry point."""
26
32
  if sysargs is None:
27
33
  sysargs = copy.deepcopy(sys.argv[1:])
@@ -39,11 +45,6 @@ def main(sysargs: list = None) -> None:
39
45
  parse_version(sysargs)
40
46
  return _exit(old_cwd=old_cwd)
41
47
 
42
- if ('-d' in sysargs or '--daemon' in sysargs) and ('stack' not in sysargs):
43
- from meerschaum.utils.daemon import daemon_entry
44
- daemon_entry(sysargs)
45
- return _exit(old_cwd=old_cwd)
46
-
47
48
  from meerschaum._internal.entry import entry, get_shell
48
49
 
49
50
  ### Try to launch a shell if --shell is provided.
@@ -57,8 +58,7 @@ def main(sysargs: list = None) -> None:
57
58
  return_tuple = entry(sysargs)
58
59
  rc = 0
59
60
  if isinstance(return_tuple, tuple) and '--nopretty' not in sysargs:
60
- from meerschaum.utils.formatting import print_tuple
61
- print_tuple(return_tuple, upper_padding=1)
61
+ _print_tuple(return_tuple, upper_padding=1)
62
62
  rc = 0 if (return_tuple[0] is True) else 1
63
63
 
64
64
  return _exit(rc, old_cwd=old_cwd)
@@ -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,
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,6 +18,59 @@ _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
+
36
+
37
+ def split_chained_sysargs(sysargs: List[str]) -> List[List[str]]:
38
+ """
39
+ Split a `sysargs` list containing "and" keys (`+`)
40
+ into a list of individual `sysargs`.
41
+ """
42
+ from meerschaum.config.static import STATIC_CONFIG
43
+ and_key = STATIC_CONFIG['system']['arguments']['and_key']
44
+
45
+ if not sysargs or and_key not in sysargs:
46
+ return [sysargs]
47
+
48
+ ### Coalesce and consecutive joiners into one.
49
+ coalesce_args = []
50
+ previous_arg = None
51
+ for arg in [_arg for _arg in sysargs]:
52
+ if arg == and_key and previous_arg == and_key:
53
+ continue
54
+ coalesce_args.append(arg)
55
+ previous_arg = arg
56
+
57
+ ### Remove any joiners from the ends.
58
+ if coalesce_args[0] == and_key:
59
+ coalesce_args = coalesce_args[1:]
60
+ if coalesce_args[-1] == and_key:
61
+ coalesce_args = coalesce_args[:-1]
62
+
63
+ chained_sysargs = []
64
+ current_sysargs = []
65
+ for arg in coalesce_args:
66
+ if arg != and_key:
67
+ current_sysargs.append(arg)
68
+ else:
69
+ chained_sysargs.append(current_sysargs)
70
+ current_sysargs = []
71
+ chained_sysargs.append(current_sysargs)
72
+ return chained_sysargs
73
+
21
74
 
22
75
  def parse_arguments(sysargs: List[str]) -> Dict[str, Any]:
23
76
  """
@@ -206,9 +259,14 @@ def parse_dict_to_sysargs(
206
259
  args_dict: Dict[str, Any]
207
260
  ) -> List[str]:
208
261
  """Revert an arguments dictionary back to a command line list."""
262
+ import shlex
209
263
  from meerschaum._internal.arguments._parser import get_arguments_triggers
210
- sysargs = []
211
- sysargs += args_dict.get('action', [])
264
+ from meerschaum.config.static import STATIC_CONFIG
265
+ from meerschaum.utils.warnings import warn
266
+
267
+ action = args_dict.get('action', None)
268
+ sysargs: List[str] = []
269
+ sysargs.extend(action or [])
212
270
  allow_none_args = {'location_keys'}
213
271
 
214
272
  triggers = get_arguments_triggers()
@@ -216,6 +274,7 @@ def parse_dict_to_sysargs(
216
274
  for a, t in triggers.items():
217
275
  if a == 'action' or a not in args_dict:
218
276
  continue
277
+
219
278
  ### Add boolean flags
220
279
  if isinstance(args_dict[a], bool):
221
280
  if args_dict[a] is True:
@@ -288,9 +347,6 @@ def remove_leading_action(
288
347
  for a in action:
289
348
  _action.append(a.replace('_', UNDERSCORE_STANDIN))
290
349
 
291
- ### e.g. 'show_pipes_baz'
292
- action_str = '_'.join(_action)
293
-
294
350
  ### e.g. 'show_pipes'
295
351
  action_name = action_function.__name__.lstrip('_')
296
352
 
@@ -300,6 +356,16 @@ def remove_leading_action(
300
356
  ### Strip away any leading prefices.
301
357
  action_name = action_name[main_action_index:]
302
358
 
359
+ subaction_parts = action_name.replace(main_action_name, '').lstrip('_').split('_')
360
+ subaction_name = subaction_parts[0] if subaction_parts else None
361
+
362
+ ### e.g. 'pipe' -> 'pipes'
363
+ if subaction_name and subaction_name.endswith('s') and not action[1].endswith('s'):
364
+ _action[1] += 's'
365
+
366
+ ### e.g. 'show_pipes_baz'
367
+ action_str = '_'.join(_action)
368
+
303
369
  if not action_str.replace(UNDERSCORE_STANDIN, '_').startswith(action_name):
304
370
  warn(f"Unable to parse '{action_str}' for action '{action_name}'.")
305
371
  return action
@@ -62,7 +62,24 @@ def parse_datetime(dt_str: str) -> Union[datetime, int, str]:
62
62
  return dt
63
63
 
64
64
 
65
- def parse_help(sysargs : Union[List[str], Dict[str, Any]]) -> None:
65
+ def parse_executor_keys(executor_keys_str: str) -> Union[str, None]:
66
+ """
67
+ Ensure that only API keys are provided for executor_keys.
68
+ """
69
+ if executor_keys_str in ('local', 'systemd'):
70
+ return executor_keys_str
71
+
72
+ if executor_keys_str.lower() == 'none':
73
+ return 'local'
74
+
75
+ if not executor_keys_str.startswith('api:'):
76
+ from meerschaum.utils.warnings import error
77
+ error(f"Invalid exectutor keys '{executor_keys_str}'.", stack=False)
78
+
79
+ return executor_keys_str
80
+
81
+
82
+ def parse_help(sysargs: Union[List[str], Dict[str, Any]]) -> None:
66
83
  """Parse the `--help` flag to determine which help message to print."""
67
84
  from meerschaum._internal.arguments._parse_arguments import parse_arguments, parse_line
68
85
  from meerschaum.actions import actions, get_subactions
@@ -135,6 +152,7 @@ _seen_plugin_args = {}
135
152
 
136
153
  groups = {}
137
154
  groups['actions'] = parser.add_argument_group(title='Actions options')
155
+ groups['jobs'] = parser.add_argument_group(title='Jobs options')
138
156
  groups['pipes'] = parser.add_argument_group(title='Pipes options')
139
157
  groups['sync'] = parser.add_argument_group(title='Sync options')
140
158
  groups['api'] = parser.add_argument_group(title='API options')
@@ -166,18 +184,25 @@ groups['actions'].add_argument(
166
184
  help="Automatically choose the defaults answers to questions. Does not result in data loss.",
167
185
  )
168
186
  groups['actions'].add_argument(
169
- '-d', '--daemon', action='store_true',
170
- help = "Run an action as a background daemon."
187
+ '-A', '--sub-args', nargs=argparse.REMAINDER,
188
+ help = (
189
+ "Provide a list of arguments for subprocesses. " +
190
+ "You can also type sub-arguments in [] instead." +
191
+ " E.g. `stack -A='--version'`, `ls [-lh]`, `echo -A these are sub-arguments`"
192
+ )
171
193
  )
172
- groups['actions'].add_argument(
173
- '--rm', action='store_true', help="Delete a job once it has finished executing."
194
+
195
+ ### Jobs options
196
+ groups['jobs'].add_argument(
197
+ '-d', '--daemon', action='store_true',
198
+ help = "Run an action as a background daemon to create a job."
174
199
  )
175
- groups['actions'].add_argument(
200
+ groups['jobs'].add_argument(
176
201
  '--name', '--job-name', type=parse_name, help=(
177
202
  "Assign a name to a job. If no name is provided, a random name will be assigned."
178
203
  ),
179
204
  )
180
- groups['actions'].add_argument(
205
+ groups['jobs'].add_argument(
181
206
  '-s', '--schedule', '--cron', type=str,
182
207
  help = (
183
208
  "Continue executing the action according to a schedule (e.g. 'every 1 seconds'). \n"
@@ -185,15 +210,20 @@ groups['actions'].add_argument(
185
210
  + "https://red-engine.readthedocs.io/en/stable/condition_syntax/index.html"
186
211
  )
187
212
  )
188
- groups['actions'].add_argument(
189
- '-A', '--sub-args', nargs=argparse.REMAINDER,
190
- help = (
191
- "Provide a list of arguments for subprocesses. " +
192
- "You can also type sub-arguments in [] instead." +
193
- " E.g. `stack -A='--version'`, `ls [-lh]`, `echo -A these are sub-arguments`"
194
- )
213
+ groups['jobs'].add_argument(
214
+ '--restart', action='store_true',
215
+ help=("Restart a job if not stopped manually."),
216
+ )
217
+ groups['jobs'].add_argument(
218
+ '-e', '--executor-keys', type=parse_executor_keys,
219
+ help=(
220
+ "Execute jobs locally or remotely. "
221
+ "Supported values are 'local', 'systemd', and 'api:{label}'."
222
+ ),
223
+ )
224
+ groups['jobs'].add_argument(
225
+ '--rm', action='store_true', help="Delete a job once it has finished executing."
195
226
  )
196
-
197
227
  ### Pipes options
198
228
  groups['pipes'].add_argument(
199
229
  '-c', '-C', '--connector-keys', nargs='+',
@@ -36,8 +36,9 @@ For your convenience, the following classes and functions may be imported from t
36
36
  <li><code>meerschaum.Connector</code></li>
37
37
  <li><code>meerschaum.Pipe</code></li>
38
38
  <li><code>meerschaum.Plugin</code></li>
39
- <li><code>meerschaum.SuccessTuple</code></li>
39
+ <li><code>meerschaum.Job</code></li>
40
40
  <li><code>meerschaum.Venv</code></li>
41
+ <li><code>meerschaum.SuccessTuple</code></li>
41
42
  </ul>
42
43
 
43
44
  </div>
@@ -54,6 +55,7 @@ For your convenience, the following classes and functions may be imported from t
54
55
  <li><code>meerschaum.make_connector()</code></li>
55
56
  <li><code>meerschaum.pprint()</code></li>
56
57
  <li><code>meerschaum.attempt_import()</code></li>
58
+ <li><code>meerschaum.entry()</code></li>
57
59
  </ul>
58
60
 
59
61
  </div>
@@ -61,7 +63,10 @@ For your convenience, the following classes and functions may be imported from t
61
63
 
62
64
  ### Examples
63
65
 
64
- #### Build a Connector
66
+ <details>
67
+ <summary><b>Build a Connector</b></summary>
68
+
69
+ Get existing connectors or build a new one in-memory with the `meerschaum.get_connector()` factory function:
65
70
 
66
71
  ```python
67
72
  import meerschaum as mrsm
@@ -80,9 +85,14 @@ sql_conn.to_sql(df, 'foo')
80
85
  print(sql_conn.read('foo'))
81
86
  # foo
82
87
  # 0 1
88
+
83
89
  ```
90
+ </details>
91
+
92
+ <details>
93
+ <summary><b>Create a Custom Connector Class</b></summary>
84
94
 
85
- #### Create a Custom Connector Class
95
+ Decorate your connector classes with `meerschaum.make_connector()` to designate it as a custom connector:
86
96
 
87
97
  ```python
88
98
  from datetime import datetime, timezone
@@ -113,8 +123,12 @@ foo_conn = mrsm.get_connector(
113
123
  )
114
124
  docs = foo_conn.fetch()
115
125
  ```
126
+ </details>
116
127
 
117
- #### Build a Pipe
128
+ <details>
129
+ <summary><b>Build a Pipe</b></summary>
130
+
131
+ Build a `meerschaum.Pipe` in-memory:
118
132
 
119
133
  ```python
120
134
  from datetime import datetime
@@ -135,7 +149,23 @@ print(df)
135
149
  # 2 2024-01-01 3 96
136
150
  ```
137
151
 
138
- #### Get Registered Pipes
152
+ Add `temporary=True` to skip registering the pipe in the pipes table.
153
+
154
+ </details>
155
+
156
+ <details>
157
+ <summary><b>Get Registered Pipes</b></summary>
158
+
159
+ The `meerschaum.get_pipes()` function returns a dictionary hierarchy of pipes by connector, metric, and location:
160
+
161
+ ```python
162
+ import meerschaum as mrsm
163
+
164
+ pipes = mrsm.get_pipes(instance='sql:temp')
165
+ pipe = pipes['foo:bar']['demo'][None]
166
+ ```
167
+
168
+ Add `as_list=True` to flatten the hierarchy:
139
169
 
140
170
  ```python
141
171
  import meerschaum as mrsm
@@ -148,8 +178,12 @@ pipes = mrsm.get_pipes(
148
178
  print(pipes)
149
179
  # [Pipe('foo:bar', 'demo', instance='sql:temp')]
150
180
  ```
181
+ </details>
151
182
 
152
- #### Access a Plugin's Module
183
+ <details>
184
+ <summary><b>Import Plugins</b></summary>
185
+
186
+ You can import a plugin's module through `meerschaum.Plugin.module`:
153
187
 
154
188
  ```python
155
189
  import meerschaum as mrsm
@@ -157,10 +191,211 @@ import meerschaum as mrsm
157
191
  plugin = mrsm.Plugin('noaa')
158
192
  with mrsm.Venv(plugin):
159
193
  noaa = plugin.module
160
- print(noaa.get_station_info('KGMU'))
161
- # {'name': 'Greenville Downtown Airport', 'geometry': {'type': 'Point', 'coordinates': [-82.35004, 34.84873]}}
162
194
  ```
163
195
 
196
+ If your plugin has submodules, use `meerschaum.plugins.from_plugin_import`:
197
+
198
+ ```python
199
+ from meerschaum.plugins import from_plugin_import
200
+ get_defined_pipes = from_plugin_import('compose.utils.pipes', 'get_defined_pipes')
201
+ ```
202
+
203
+ Import multiple plugins with `meerschaum.plugins.import_plugins`:
204
+
205
+ ```python
206
+ from meerschaum.plugins import import_plugins
207
+ noaa, compose = import_plugins('noaa', 'compose')
208
+ ```
209
+
210
+ </details>
211
+
212
+ <details>
213
+ <summary><b>Create a Job</b></summary>
214
+
215
+ Create a `meerschaum.Job` with `name` and `sysargs`:
216
+
217
+ ```python
218
+ import meerschaum as mrsm
219
+
220
+ job = mrsm.Job('syncing-engine', 'sync pipes --loop')
221
+ success, msg = job.start()
222
+ ```
223
+
224
+ Pass `executor_keys` as the connectors keys of an API instance to create a remote job:
225
+
226
+ ```python
227
+ import meerschaum as mrsm
228
+
229
+ job = mrsm.Job(
230
+ 'foo',
231
+ 'sync pipes -s daily',
232
+ executor_keys='api:main',
233
+ )
234
+ ```
235
+
236
+ </details>
237
+
238
+ <details>
239
+ <summary><b>Import from a Virtual Environment</b></summary>
240
+ Use the `meerschaum.Venv` context manager to activate a virtual environment:
241
+ ```python
242
+ import meerschaum as mrsm
243
+
244
+ with mrsm.Venv('noaa'):
245
+ import requests
246
+
247
+ print(requests.__file__)
248
+ # /home/bmeares/.config/meerschaum/venvs/noaa/lib/python3.12/site-packages/requests/__init__.py
249
+ ```
250
+
251
+ To import packages which may not be installed, use `meerschaum.attempt_import()`:
252
+
253
+ ```python
254
+ import meerschaum as mrsm
255
+
256
+ requests = mrsm.attempt_import('requests', venv='noaa')
257
+ print(requests.__file__)
258
+ # /home/bmeares/.config/meerschaum/venvs/noaa/lib/python3.12/site-packages/requests/__init__.py
259
+ ```
260
+
261
+ </details>
262
+
263
+ <details>
264
+ <summary><b>Run Actions</b></summary>
265
+
266
+ Run `sysargs` with `meerschaum.entry()`:
267
+
268
+ ```python
269
+ import meerschaum as mrsm
270
+
271
+ success, msg = mrsm.entry('show pipes + show version : x2')
272
+ ```
273
+
274
+ Use `meerschaum.actions.get_action()` to access an action function directly:
275
+
276
+ ```python
277
+ from meerschaum.actions import get_action
278
+
279
+ show_pipes = get_action(['show', 'pipes'])
280
+ success, msg = show_pipes(connector_keys=['plugin:noaa'])
281
+ ```
282
+
283
+ Get a dictionary of available subactions with `meerschaum.actions.get_subactions()`:
284
+
285
+ ```python
286
+ from meerschaum.actions import get_subactions
287
+
288
+ subactions = get_subactions('show')
289
+ success, msg = subactions['pipes']()
290
+ ```
291
+
292
+ </details>
293
+
294
+ <details>
295
+ <summary><b>Create a Plugin</b></summary>
296
+
297
+ Run `bootstrap plugin` to create a new plugin:
298
+
299
+ ```
300
+ mrsm bootstrap plugin example
301
+ ```
302
+
303
+ This will create `example.py` in your plugins directory (default `~/.config/meerschaum/plugins/`, Windows: `%APPDATA%\Meerschaum\plugins`). You may paste the example code from the "Create a Custom Action" example below.
304
+
305
+ Open your plugin with `edit plugin`:
306
+
307
+ ```
308
+ mrsm edit plugin example
309
+ ```
310
+
311
+ *Run `edit plugin` and paste the example code below to try out the features.*
312
+
313
+ See the [writing plugins guide](https://meerschaum.io/reference/plugins/writing-plugins/) for more in-depth documentation.
314
+
315
+ </details>
316
+
317
+ <details>
318
+ <summary><b>Create a Custom Action</b></summary>
319
+
320
+ Decorate a function with `meerschaum.actions.make_action` to designate it as an action. Subactions will be automatically detected if not decorated:
321
+
322
+ ```python
323
+ from meerschaum.actions import make_action
324
+
325
+ @make_action
326
+ def sing():
327
+ print('What would you like me to sing?')
328
+ return True, "Success"
329
+
330
+ def sing_tune():
331
+ return False, "I don't know that song!"
332
+
333
+ def sing_song():
334
+ print('Hello, World!')
335
+ return True, "Success"
336
+
337
+ ```
338
+
339
+ Use `meerschaum.plugins.add_plugin_argument()` to create new parameters for your action:
340
+
341
+ ```python
342
+ from meerschaum.plugins import make_action, add_plugin_argument
343
+
344
+ add_plugin_argument(
345
+ '--song', type=str, help='What song to sing.',
346
+ )
347
+
348
+ @make_action
349
+ def sing_melody(action=None, song=None):
350
+ to_sing = action[0] if action else song
351
+ if not to_sing:
352
+ return False, "Please tell me what to sing!"
353
+
354
+ return True, f'~I am singing {to_sing}~'
355
+ ```
356
+
357
+ ```
358
+ mrsm sing melody lalala
359
+
360
+ mrsm sing melody --song do-re-mi
361
+ ```
362
+
363
+ </details>
364
+
365
+ <details>
366
+ <summary><b>Add a Page to the Web Dashboard</b></summary>
367
+ Use the decorators `meerschaum.plugins.dash_plugin()` and `meerschaum.plugins.web_page()` to add new pages to the web dashboard:
368
+
369
+ ```python
370
+ from meerschaum.plugins import dash_plugin, web_page
371
+
372
+ @dash_plugin
373
+ def init_dash(dash_app):
374
+
375
+ import dash.html as html
376
+ import dash_bootstrap_components as dbc
377
+ from dash import Input, Output, no_update
378
+
379
+ ### Routes to '/dash/my-page'
380
+ @web_page('/my-page', login_required=False)
381
+ def my_page():
382
+ return dbc.Container([
383
+ html.H1("Hello, World!"),
384
+ dbc.Button("Click me", id='my-button'),
385
+ html.Div(id="my-output-div"),
386
+ ])
387
+
388
+ @dash_app.callback(
389
+ Output('my-output-div', 'children'),
390
+ Input('my-button', 'n_clicks'),
391
+ )
392
+ def my_button_click(n_clicks):
393
+ if not n_clicks:
394
+ return no_update
395
+ return html.P(f'You clicked {n_clicks} times!')
396
+ ```
397
+ </details>
398
+
164
399
  ## Submodules
165
400
 
166
401
  <details>
@@ -206,6 +441,28 @@ with mrsm.Venv(plugin):
206
441
 
207
442
  </details>
208
443
 
444
+ <details>
445
+ <summary>
446
+ `meerschaum.jobs`<br>
447
+ Start background jobs.
448
+ </summary>
449
+
450
+ - `meerschaum.jobs.Job`
451
+ - `meerschaum.jobs.Executor`
452
+ - `meerschaum.jobs.systemd.SystemdExecutor`
453
+ - `meerschaum.jobs.get_jobs()`
454
+ - `meerschaum.jobs.get_filtered_jobs()`
455
+ - `meerschaum.jobs.get_running_jobs()`
456
+ - `meerschaum.jobs.get_stopped_jobs()`
457
+ - `meerschaum.jobs.get_paused_jobs()`
458
+ - `meerschaum.jobs.get_restart_jobs()`
459
+ - `meerschaum.jobs.make_executor()`
460
+ - `meerschaum.jobs.check_restart_jobs()`
461
+ - `meerschaum.jobs.start_check_jobs_thread()`
462
+ - `meerschaum.jobs.stop_check_jobs_thread()`
463
+
464
+ </details>
465
+
209
466
  <details>
210
467
  <summary>
211
468
  `meerschaum.plugins`<br>