meerschaum 2.9.4__py3-none-any.whl → 3.0.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 (201) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +228 -117
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +438 -88
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
meerschaum/__init__.py CHANGED
@@ -3,7 +3,7 @@
3
3
  # vim:fenc=utf-8
4
4
 
5
5
  """
6
- Copyright 2023 Bennett Meares
6
+ Copyright 2025 Bennett Meares
7
7
 
8
8
  Licensed under the Apache License, Version 2.0 (the "License");
9
9
  you may not use this file except in compliance with the License.
@@ -19,13 +19,14 @@ limitations under the License.
19
19
  """
20
20
 
21
21
  import atexit
22
+
22
23
  from meerschaum.utils.typing import SuccessTuple
23
24
  from meerschaum.utils.packages import attempt_import
24
25
  from meerschaum.core.Pipe import Pipe
25
26
  from meerschaum.plugins import Plugin
26
27
  from meerschaum.utils.venv import Venv
27
28
  from meerschaum.jobs import Job, make_executor
28
- from meerschaum.connectors import get_connector, Connector, make_connector
29
+ from meerschaum.connectors import get_connector, Connector, InstanceConnector, make_connector
29
30
  from meerschaum.utils import get_pipes
30
31
  from meerschaum.utils.formatting import pprint
31
32
  from meerschaum._internal.docs import index as __doc__
@@ -42,6 +43,7 @@ __all__ = (
42
43
  "get_config",
43
44
  "Pipe",
44
45
  "Plugin",
46
+ "SuccessTuple",
45
47
  "Venv",
46
48
  "Plugin",
47
49
  "Job",
@@ -55,6 +57,7 @@ __all__ = (
55
57
  "utils",
56
58
  "SuccessTuple",
57
59
  "Connector",
60
+ "InstanceConnector",
58
61
  "make_connector",
59
62
  "entry",
60
63
  )
@@ -6,3 +6,4 @@
6
6
  Modules, classes, and variables declared in _internal are not intended for general public use.
7
7
  """
8
8
 
9
+ from meerschaum._internal.static import STATIC_CONFIG
@@ -23,7 +23,7 @@ def split_pipeline_sysargs(sysargs: List[str]) -> Tuple[List[str], List[str]]:
23
23
  """
24
24
  Split `sysargs` into the main pipeline and the flags following the pipeline separator (`:`).
25
25
  """
26
- from meerschaum.config.static import STATIC_CONFIG
26
+ from meerschaum._internal.static import STATIC_CONFIG
27
27
  pipeline_key = STATIC_CONFIG['system']['arguments']['pipeline_key']
28
28
  if pipeline_key not in sysargs:
29
29
  return sysargs, []
@@ -40,7 +40,7 @@ def split_chained_sysargs(sysargs: List[str]) -> List[List[str]]:
40
40
  Split a `sysargs` list containing "and" keys (`+`)
41
41
  into a list of individual `sysargs`.
42
42
  """
43
- from meerschaum.config.static import STATIC_CONFIG
43
+ from meerschaum._internal.static import STATIC_CONFIG
44
44
  and_key = STATIC_CONFIG['system']['arguments']['and_key']
45
45
 
46
46
  if not sysargs or and_key not in sysargs:
@@ -90,7 +90,7 @@ def parse_arguments(sysargs: List[str]) -> Dict[str, Any]:
90
90
 
91
91
  """
92
92
  import shlex
93
- from meerschaum.config.static import STATIC_CONFIG
93
+ from meerschaum._internal.static import STATIC_CONFIG
94
94
  from meerschaum._internal.arguments._parser import parser
95
95
 
96
96
  global _loaded_plugins_args
@@ -360,7 +360,7 @@ def remove_leading_action(
360
360
  """
361
361
  from meerschaum.actions import get_action, get_main_action_name
362
362
  from meerschaum.utils.warnings import warn
363
- from meerschaum.config.static import STATIC_CONFIG
363
+ from meerschaum._internal.static import STATIC_CONFIG
364
364
  action_function = get_action(action, _actions=_actions)
365
365
  if action_function is None:
366
366
  return action
@@ -40,7 +40,8 @@ class ArgumentParser(argparse.ArgumentParser):
40
40
 
41
41
  def parse_datetime(dt_str: str) -> Union[datetime, int, str]:
42
42
  """Parse a string into a datetime."""
43
- from meerschaum.utils.misc import is_int, round_time
43
+ from meerschaum.utils.dtypes import round_time
44
+ from meerschaum.utils.misc import is_int
44
45
  if is_int(dt_str):
45
46
  return int(dt_str)
46
47
 
@@ -139,7 +140,11 @@ def parse_help(sysargs: Union[List[str], Dict[str, Any]]) -> None:
139
140
  ### Check for subactions.
140
141
  if len(args['action']) > 1:
141
142
  try:
142
- subaction = get_subactions(args['action'][0])[args['action'][1]]
143
+ subactions = get_subactions(args['action'][0])
144
+ subaction_name = args['action'][1]
145
+ if subaction_name not in subactions:
146
+ subaction_name = subaction_name + 's'
147
+ subaction = subactions[subaction_name]
143
148
  except Exception:
144
149
  subaction = None
145
150
  if subaction is not None:
@@ -195,6 +200,7 @@ groups['pipes'] = parser.add_argument_group(title='Pipes options')
195
200
  groups['sync'] = parser.add_argument_group(title='Sync options')
196
201
  groups['api'] = parser.add_argument_group(title='API options')
197
202
  groups['plugins'] = parser.add_argument_group(title='Plugins options')
203
+ groups['tokens'] = parser.add_argument_group(title='Tokens options')
198
204
  groups['packages'] = parser.add_argument_group(title='Packages options')
199
205
  groups['debug'] = parser.add_argument_group(title='Debugging options')
200
206
  groups['misc'] = parser.add_argument_group(title='Miscellaneous options')
@@ -375,7 +381,7 @@ groups['sync'].add_argument(
375
381
  groups['sync'].add_argument(
376
382
  '--cache', action='store_true',
377
383
  help=(
378
- "When syncing or viewing a pipe's data, sync to a local database for later analysis."
384
+ "Sync pipes' metadata to disk."
379
385
  )
380
386
  )
381
387
 
@@ -387,13 +393,21 @@ groups['api'].add_argument(
387
393
  '--host', type=str,
388
394
  help="The host address to bind to for the API server. Defaults to '0.0.0.0'."
389
395
  )
396
+ groups['api'].add_argument(
397
+ '--webterm-port', type=int,
398
+ help="The port on which to run the webterm server.",
399
+ )
390
400
  groups['api'].add_argument(
391
401
  '-w', '--workers', type=int,
392
402
  help = "How many workers to run a concurrent action (e.g. running the API or syncing pipes)"
393
403
  )
394
404
  groups['api'].add_argument(
395
405
  '--no-dash', '--nodash', action='store_true',
396
- help = 'When starting the API, do not start the Web interface.',
406
+ help = 'When starting the API, do not start the Web Console dashboard.',
407
+ )
408
+ groups['api'].add_argument(
409
+ '--no-webterm', '--nowebterm', action='store_true',
410
+ help="When starting the API, do not start the Webterm.",
397
411
  )
398
412
  groups['api'].add_argument(
399
413
  '--private', '--private-mode', action='store_true',
@@ -419,12 +433,23 @@ groups['api'].add_argument(
419
433
  '--certfile', type=str,
420
434
  help = "Start the API server with this certfile (requires --keyfile).",
421
435
  )
436
+
422
437
  ### Plugins options
423
438
  groups['plugins'].add_argument(
424
439
  '-r', '--repository', '--repo', type=str,
425
440
  help="Meerschaum plugins repository to connect to. Specify an API label (default: 'mrsm')"
426
441
  )
427
442
 
443
+ ### Tokens options
444
+ groups['tokens'].add_argument(
445
+ '--ttl-days', type=int,
446
+ help="For how many days should a token be valid.",
447
+ )
448
+ groups['tokens'].add_argument(
449
+ '--scopes', '--scope', nargs='+',
450
+ help="Which scope permissions to grant to a token.",
451
+ )
452
+
428
453
  ### Packages options
429
454
  groups['packages'].add_argument(
430
455
  '--venv', type=str,
@@ -491,3 +516,7 @@ groups['misc'].add_argument(
491
516
  + "Default behavior is to only allow recognized actions."
492
517
  )
493
518
  )
519
+ groups['misc'].add_argument(
520
+ '--no-daemon', action='store_true',
521
+ help="Do not use the Meerschaum CLI daemon to execute the action.",
522
+ )
@@ -0,0 +1,6 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define internal utilities for interacting with the CLI daemons.
6
+ """
@@ -0,0 +1,103 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the CLI daemons utilities.
6
+ """
7
+
8
+ import os
9
+ import threading
10
+ import pathlib
11
+ from typing import Optional, List
12
+
13
+ import meerschaum as mrsm
14
+
15
+
16
+ def get_cli_daemon(ix: Optional[int] = None):
17
+ """
18
+ Get the CLI daemon.
19
+ """
20
+ from meerschaum._internal.cli.workers import ActionWorker
21
+ ix = ix if ix is not None else get_available_cli_daemon_ix()
22
+ return ActionWorker(ix)
23
+
24
+
25
+ def get_cli_lock_path(ix: int) -> pathlib.Path:
26
+ """
27
+ Return the path to a CLI daemon's lock file.
28
+ """
29
+ from meerschaum.config.paths import CLI_RESOURCES_PATH
30
+ return CLI_RESOURCES_PATH / f"ix-{ix}.lock"
31
+
32
+
33
+ def get_cli_session_id() -> str:
34
+ """
35
+ Return the session ID to use for the current process and thread.
36
+ """
37
+ return f"{os.getpid()}.{threading.current_thread().ident}"
38
+
39
+
40
+ def get_available_cli_daemon_ix() -> int:
41
+ """
42
+ Return the index for an available CLI daemon.
43
+ """
44
+ max_ix = mrsm.get_config('system', 'cli', 'max_daemons') - 1
45
+ ix = 0
46
+ while True:
47
+ lock_path = get_cli_lock_path(ix)
48
+ if not lock_path.exists():
49
+ return ix
50
+
51
+ ix += 1
52
+ if ix > max_ix:
53
+ raise EnvironmentError("Too many CLI daemons are running.")
54
+
55
+
56
+ def get_existing_cli_daemon_indices() -> List[int]:
57
+ """
58
+ Return a list of the existing CLI daemons.
59
+ """
60
+ from meerschaum.utils.daemon import get_daemon_ids
61
+ daemon_ids = [daemon_id for daemon_id in get_daemon_ids() if daemon_id.startswith('.cli.')]
62
+ indices = []
63
+
64
+ for daemon_id in daemon_ids:
65
+ try:
66
+ ix = int(daemon_id[len('.cli.'):])
67
+ indices.append(ix)
68
+ except Exception:
69
+ pass
70
+
71
+ return indices
72
+
73
+
74
+ def get_existing_cli_daemons() -> 'List[ActionWorker]':
75
+ """
76
+ Return a list of the existing CLI daemons.
77
+ """
78
+ from meerschaum._internal.cli.workers import ActionWorker
79
+ indices = get_existing_cli_daemon_indices()
80
+ return [
81
+ ActionWorker(ix)
82
+ for ix in indices
83
+ ]
84
+
85
+
86
+ def _get_cli_session_dir_path(session_id: str) -> pathlib.Path:
87
+ """
88
+ Return the path to the file handles for the CLI session.
89
+ """
90
+ from meerschaum.config.paths import CLI_RESOURCES_PATH
91
+ return CLI_RESOURCES_PATH / session_id
92
+
93
+
94
+ def _get_cli_stream_path(
95
+ session_id: str,
96
+ action_id: str,
97
+ stream: str = 'stdout',
98
+ ) -> pathlib.Path:
99
+ """
100
+ Return the file path for the stdout / stderr file stream.
101
+ """
102
+ session_dir_path = _get_cli_session_dir_path(session_id)
103
+ return session_dir_path / f'{action_id}.{stream}'
@@ -0,0 +1,220 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Define the CLI daemon entrypoint.
6
+ """
7
+
8
+ import os
9
+ import time
10
+ import uuid
11
+ import shlex
12
+ import shutil
13
+ import traceback
14
+ import signal
15
+ from typing import Optional, Dict, List, Any
16
+
17
+ import meerschaum as mrsm
18
+
19
+
20
+ def entry_with_daemon(
21
+ sysargs: Optional[List[str]] = None,
22
+ _patch_args: Optional[Dict[str, Any]] = None,
23
+ _use_cli_daemon: bool = True,
24
+ _session_id: Optional[str] = None,
25
+ ) -> mrsm.SuccessTuple:
26
+ """
27
+ Parse arguments and launch a Meerschaum action.
28
+
29
+ Returns
30
+ -------
31
+ A `SuccessTuple` indicating success.
32
+ """
33
+ from meerschaum.actions import get_action
34
+ from meerschaum.plugins import load_plugins, _actions_daemon_enabled
35
+ from meerschaum._internal.entry import entry_without_daemon
36
+ from meerschaum._internal.cli.workers import ActionWorker
37
+ from meerschaum._internal.cli.daemons import (
38
+ get_available_cli_daemon_ix,
39
+ get_cli_session_id,
40
+ )
41
+ from meerschaum.config import get_possible_keys
42
+ from meerschaum._internal.arguments import split_pipeline_sysargs, split_chained_sysargs
43
+ daemon_is_ready = True
44
+
45
+ load_plugins(skip_if_loaded=True)
46
+
47
+ found_acceptable_prefix = False
48
+ found_unacceptable_prefix = False
49
+ found_disabled_action = False
50
+ allowed_prefixes = (
51
+ mrsm.get_config('system', 'cli', 'allowed_prefixes')
52
+ )
53
+ disallowed_prefixes = (
54
+ mrsm.get_config('system', 'cli', 'disallowed_prefixes')
55
+ )
56
+ refresh_seconds = mrsm.get_config('system', 'cli', 'refresh_seconds')
57
+ sysargs_str = sysargs if isinstance(sysargs, str) else shlex.join(sysargs or [])
58
+ debug = ' --debug' in sysargs_str
59
+ _sysargs = shlex.split(sysargs_str)
60
+ _sysargs, _pipeline_args = split_pipeline_sysargs(_sysargs)
61
+ _chained_sysargs = split_chained_sysargs(_sysargs)
62
+ _action_functions = {
63
+ _action_func.__name__: _action_func
64
+ for _step_sysargs in _chained_sysargs
65
+ if (_action_func := get_action(_step_sysargs))
66
+ }
67
+ for action_name, enabled in _actions_daemon_enabled.items():
68
+ if action_name not in _action_functions:
69
+ continue
70
+ if enabled:
71
+ continue
72
+ found_disabled_action = True
73
+ break
74
+
75
+ for prefix in allowed_prefixes:
76
+ if sysargs_str.startswith(prefix) or prefix == '*':
77
+ found_acceptable_prefix = True
78
+ break
79
+
80
+ for prefix in disallowed_prefixes:
81
+ if sysargs_str.startswith(prefix) or prefix == '*':
82
+ found_unacceptable_prefix = True
83
+ break
84
+
85
+ if not found_acceptable_prefix or found_unacceptable_prefix or found_disabled_action:
86
+ daemon_is_ready = False
87
+
88
+ try:
89
+ daemon_ix = get_available_cli_daemon_ix() if daemon_is_ready else -1
90
+ except EnvironmentError as e:
91
+ from meerschaum.utils.warnings import warn
92
+ warn(e, stack=False)
93
+ daemon_ix = -1
94
+ daemon_is_ready = False
95
+
96
+ worker = ActionWorker(daemon_ix, refresh_seconds=refresh_seconds) if daemon_ix != -1 else None
97
+ if worker and worker.lock_path.exists():
98
+ daemon_is_ready = False
99
+
100
+ start_success, start_msg = (
101
+ worker.job.start()
102
+ if worker and daemon_is_ready
103
+ else (False, "Lock exists.")
104
+ )
105
+
106
+ if not start_success:
107
+ daemon_is_ready = False
108
+
109
+ if start_success and worker:
110
+ start = time.perf_counter()
111
+ while (time.perf_counter() - start) < 3:
112
+ if worker.is_ready():
113
+ break
114
+ time.sleep(refresh_seconds)
115
+
116
+ if not daemon_is_ready or worker is None:
117
+ if debug:
118
+ print("Revert to entry without daemon.")
119
+ return entry_without_daemon(sysargs, _patch_args=_patch_args)
120
+
121
+ session_id = _session_id or get_cli_session_id()
122
+ action_id = uuid.uuid4().hex
123
+
124
+ terminal_size = shutil.get_terminal_size()
125
+ env = {
126
+ **{
127
+ 'LINES': str(terminal_size.lines),
128
+ 'COLUMNS': str(terminal_size.columns),
129
+ },
130
+ **dict(os.environ),
131
+ }
132
+ for key in get_possible_keys():
133
+ _ = mrsm.get_config(key)
134
+ config = mrsm.get_config()
135
+
136
+ worker.write_input_data({
137
+ 'session_id': session_id,
138
+ 'action_id': action_id,
139
+ 'sysargs': sysargs,
140
+ 'patch_args': _patch_args,
141
+ 'env': env,
142
+ 'config': config,
143
+ })
144
+
145
+ accepted = False
146
+ exit_data = None
147
+ worker_data = None
148
+
149
+ while not accepted:
150
+ state = worker.read_output_data().get('state', None)
151
+ if state == 'accepted':
152
+ accepted = True
153
+ break
154
+
155
+ time.sleep(refresh_seconds)
156
+
157
+ worker.start_cli_logs_refresh_thread()
158
+
159
+ try:
160
+ log = worker.job.daemon.rotating_log
161
+ worker.job.monitor_logs(
162
+ stop_on_exit=True,
163
+ callback_function=worker.monitor_callback,
164
+ _log=log,
165
+ _wait_if_stopped=False,
166
+ )
167
+ worker_data = worker.read_output_data()
168
+ except KeyboardInterrupt:
169
+ exit_data = {
170
+ 'session_id': session_id,
171
+ 'action_id': action_id,
172
+ 'signal': signal.SIGINT,
173
+ 'traceback': traceback.format_exc(),
174
+ 'success': True,
175
+ 'message': 'Exiting due to keyboard interrupt.',
176
+ }
177
+ except Exception as e:
178
+ exit_data = {
179
+ 'session_id': session_id,
180
+ 'action_id': action_id,
181
+ 'signal': signal.SIGINT,
182
+ 'traceback': traceback.format_exc(),
183
+ 'success': False,
184
+ 'message': f'Encountered exception: {e}',
185
+ }
186
+ except SystemExit:
187
+ exit_data = {
188
+ 'session_id': session_id,
189
+ 'action_id': action_id,
190
+ 'signal': signal.SIGTERM,
191
+ 'traceback': traceback.format_exc(),
192
+ 'success': True,
193
+ 'message': 'Exiting on SIGTERM.',
194
+ }
195
+ except BrokenPipeError:
196
+ return False, "Connection to daemon is broken."
197
+
198
+ if exit_data:
199
+ exit_success = bool(exit_data['success'])
200
+ exit_signal = exit_data['signal']
201
+ worker.send_signal(exit_signal)
202
+ try:
203
+ worker.job.monitor_logs(
204
+ stop_on_exit=True,
205
+ callback_function=worker.monitor_callback,
206
+ _wait_if_stopped=False,
207
+ _log=log,
208
+ )
209
+ except (KeyboardInterrupt, SystemExit):
210
+ worker.send_signal(signal.SIGTERM)
211
+ worker_data = worker.read_output_data()
212
+
213
+ if not exit_success:
214
+ print(exit_data['traceback'])
215
+
216
+ worker.stop_cli_logs_refresh_thread()
217
+ worker.write_input_data({'increment': True})
218
+ success = (worker_data or {}).get('success', False)
219
+ message = (worker_data or {}).get('message', "Failed to retrieve message from CLI worker.")
220
+ return success, message