meerschaum 2.2.6__py3-none-any.whl → 2.3.0.dev1__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 (61) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/__main__.py +10 -5
  3. meerschaum/_internal/arguments/_parser.py +44 -15
  4. meerschaum/_internal/entry.py +35 -14
  5. meerschaum/_internal/shell/Shell.py +155 -53
  6. meerschaum/_internal/shell/updates.py +175 -0
  7. meerschaum/actions/api.py +12 -12
  8. meerschaum/actions/attach.py +95 -0
  9. meerschaum/actions/delete.py +35 -26
  10. meerschaum/actions/register.py +19 -5
  11. meerschaum/actions/show.py +119 -148
  12. meerschaum/actions/start.py +85 -75
  13. meerschaum/actions/stop.py +68 -39
  14. meerschaum/actions/sync.py +3 -3
  15. meerschaum/actions/upgrade.py +28 -36
  16. meerschaum/api/_events.py +18 -1
  17. meerschaum/api/_oauth2.py +2 -0
  18. meerschaum/api/_websockets.py +2 -2
  19. meerschaum/api/dash/jobs.py +5 -2
  20. meerschaum/api/routes/__init__.py +1 -0
  21. meerschaum/api/routes/_actions.py +122 -44
  22. meerschaum/api/routes/_jobs.py +340 -0
  23. meerschaum/api/routes/_pipes.py +25 -25
  24. meerschaum/config/_default.py +1 -0
  25. meerschaum/config/_formatting.py +1 -0
  26. meerschaum/config/_paths.py +5 -0
  27. meerschaum/config/_shell.py +84 -67
  28. meerschaum/config/_version.py +1 -1
  29. meerschaum/config/static/__init__.py +9 -0
  30. meerschaum/connectors/__init__.py +9 -11
  31. meerschaum/connectors/api/APIConnector.py +18 -1
  32. meerschaum/connectors/api/_actions.py +60 -71
  33. meerschaum/connectors/api/_jobs.py +260 -0
  34. meerschaum/connectors/api/_misc.py +1 -1
  35. meerschaum/connectors/api/_request.py +13 -9
  36. meerschaum/connectors/parse.py +23 -7
  37. meerschaum/core/Pipe/_sync.py +3 -0
  38. meerschaum/plugins/__init__.py +89 -5
  39. meerschaum/utils/daemon/Daemon.py +333 -149
  40. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  41. meerschaum/utils/daemon/RotatingFile.py +18 -7
  42. meerschaum/utils/daemon/StdinFile.py +110 -0
  43. meerschaum/utils/daemon/__init__.py +40 -27
  44. meerschaum/utils/formatting/__init__.py +83 -37
  45. meerschaum/utils/formatting/_jobs.py +118 -51
  46. meerschaum/utils/formatting/_shell.py +6 -0
  47. meerschaum/utils/jobs/_Job.py +684 -0
  48. meerschaum/utils/jobs/__init__.py +245 -0
  49. meerschaum/utils/misc.py +18 -17
  50. meerschaum/utils/packages/__init__.py +21 -15
  51. meerschaum/utils/packages/_packages.py +2 -2
  52. meerschaum/utils/prompt.py +20 -7
  53. meerschaum/utils/schedule.py +21 -15
  54. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/METADATA +9 -9
  55. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/RECORD +61 -54
  56. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/WHEEL +1 -1
  57. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/LICENSE +0 -0
  58. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/NOTICE +0 -0
  59. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/entry_points.txt +0 -0
  60. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/top_level.txt +0 -0
  61. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/zip-safe +0 -0
@@ -0,0 +1,175 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ If configured, check `api:mrsm` for announcement messages.
7
+ """
8
+
9
+ import json
10
+ from datetime import datetime, timezone, timedelta
11
+
12
+ import meerschaum as mrsm
13
+ from meerschaum.utils.typing import Union, SuccessTuple, Optional
14
+ from meerschaum.config import get_config
15
+ from meerschaum.utils.formatting import CHARSET, ANSI, colored
16
+ from meerschaum.utils.misc import string_width, remove_ansi
17
+ from meerschaum.config.paths import (
18
+ UPDATES_LOCK_PATH,
19
+ UPDATES_CACHE_PATH,
20
+ )
21
+ from meerschaum.utils.threading import Thread
22
+
23
+
24
+ def cache_remote_version(debug: bool = False) -> SuccessTuple:
25
+ """
26
+ Fetch and cache the latest version if available.
27
+ """
28
+ allow_update_check = get_config('shell', 'updates', 'check_remote')
29
+ if not allow_update_check:
30
+ return True, "Update checks are disabled."
31
+
32
+ refresh_minutes = get_config('shell', 'updates', 'refresh_minutes')
33
+ update_delta = timedelta(minutes=refresh_minutes)
34
+
35
+ if UPDATES_CACHE_PATH.exists():
36
+ try:
37
+ with open(UPDATES_CACHE_PATH, 'r', encoding='utf8') as f:
38
+ cache_dict = json.load(f)
39
+ except Exception:
40
+ cache_dict = {}
41
+ else:
42
+ cache_dict = {}
43
+
44
+ now = datetime.now(timezone.utc)
45
+ last_check_ts_str = cache_dict.get('last_check_ts')
46
+ last_check_ts = datetime.fromisoformat(last_check_ts_str) if last_check_ts_str else None
47
+
48
+ need_update = (
49
+ last_check_ts_str is None
50
+ or ((now - last_check_ts) >= update_delta)
51
+ )
52
+
53
+ if not need_update:
54
+ return True, "No updates are needed."
55
+
56
+ try:
57
+ conn = mrsm.get_connector('api:mrsm')
58
+ remote_version = conn.get_mrsm_version(debug=debug, timeout=3)
59
+ except Exception:
60
+ remote_version = None
61
+
62
+ if remote_version is None:
63
+ return False, "Could not determine remote version."
64
+
65
+ with open(UPDATES_CACHE_PATH, 'w+', encoding='utf-8') as f:
66
+ json.dump(
67
+ {
68
+ 'last_check_ts': now.isoformat(),
69
+ 'remote_version': remote_version,
70
+ },
71
+ f,
72
+ )
73
+
74
+ return True, "Updated remote version cache."
75
+
76
+
77
+ def run_version_check_thread(debug: bool = False) -> Union[Thread, None]:
78
+ """
79
+ Run the version update check in a separate thread.
80
+ """
81
+ allow_update_check = get_config('shell', 'updates', 'check_remote')
82
+ if not allow_update_check:
83
+ return None
84
+
85
+ thread = Thread(
86
+ target=cache_remote_version,
87
+ daemon=True,
88
+ kwargs={'debug': debug},
89
+ )
90
+ thread.start()
91
+ return thread
92
+
93
+
94
+ _remote_version: Optional[str] = None
95
+ def get_remote_version_from_cache() -> Optional[str]:
96
+ """
97
+ Return the version string from the local cache file.
98
+ """
99
+ global _remote_version
100
+ try:
101
+ with open(UPDATES_CACHE_PATH, 'r', encoding='utf-8') as f:
102
+ cache_dict = json.load(f)
103
+ except Exception:
104
+ return None
105
+
106
+ _remote_version = cache_dict.get('remote_version')
107
+ return _remote_version
108
+
109
+
110
+ _out_of_date: Optional[bool] = None
111
+ def mrsm_out_of_date() -> bool:
112
+ """
113
+ Determine whether to print the upgrade message.
114
+ """
115
+ global _out_of_date
116
+ if _out_of_date is not None:
117
+ return _out_of_date
118
+
119
+ ### NOTE: Remote version is cached asynchronously.
120
+ if not UPDATES_CACHE_PATH.exists():
121
+ return False
122
+
123
+ remote_version_str = get_remote_version_from_cache()
124
+
125
+ packaging_version = mrsm.attempt_import('packaging.version')
126
+ current_version = packaging_version.parse(mrsm.__version__)
127
+ remote_version = packaging_version.parse(remote_version_str)
128
+
129
+ _out_of_date = remote_version > current_version
130
+ return _out_of_date
131
+
132
+
133
+ def get_update_message() -> str:
134
+ """
135
+ Return the formatted message for when the current version is behind the latest release.
136
+ """
137
+ if not mrsm_out_of_date():
138
+ return ''
139
+
140
+ intro = get_config('shell', CHARSET, 'intro')
141
+ update_message = get_config('shell', CHARSET, 'update_message')
142
+ remote_version = get_remote_version_from_cache()
143
+ if not remote_version:
144
+ return ''
145
+
146
+ intro_width = string_width(intro)
147
+ msg_width = string_width(update_message)
148
+ update_left_padding = ' ' * ((intro_width - msg_width) // 2)
149
+
150
+ update_line = (
151
+ colored(
152
+ update_message,
153
+ *get_config('shell', 'ansi', 'update_message', 'color')
154
+ ) if ANSI
155
+ else update_message
156
+ )
157
+ update_instruction = (
158
+ colored("Run ", 'white')
159
+ + colored("upgrade mrsm", 'green')
160
+ + colored(" to install ", 'white')
161
+ + colored(f'v{remote_version}', 'yellow')
162
+ + '.'
163
+ )
164
+ update_instruction_clean = remove_ansi(update_instruction)
165
+ instruction_width = string_width(update_instruction_clean)
166
+ instruction_left_padding = ' ' * ((intro_width - instruction_width) // 2)
167
+
168
+ return (
169
+ '\n\n'
170
+ + update_left_padding
171
+ + update_line
172
+ + '\n'
173
+ + instruction_left_padding
174
+ + update_instruction
175
+ )
meerschaum/actions/api.py CHANGED
@@ -10,12 +10,12 @@ import os
10
10
  from meerschaum.utils.typing import SuccessTuple, Optional, List, Any
11
11
 
12
12
  def api(
13
- action: Optional[List[str]] = None,
14
- sysargs: Optional[List[str]] = None,
15
- debug: bool = False,
16
- mrsm_instance: Optional[str] = None,
17
- **kw: Any
18
- ) -> SuccessTuple:
13
+ action: Optional[List[str]] = None,
14
+ sysargs: Optional[List[str]] = None,
15
+ debug: bool = False,
16
+ mrsm_instance: Optional[str] = None,
17
+ **kw: Any
18
+ ) -> SuccessTuple:
19
19
  """
20
20
  Send commands to a Meerschaum WebAPI instance.
21
21
 
@@ -37,7 +37,8 @@ def api(
37
37
  """
38
38
  from meerschaum.utils.warnings import warn, info
39
39
  from meerschaum.utils.formatting import print_tuple
40
- from meerschaum.utils.packages import attempt_import
40
+ from meerschaum._internal.arguments._parse_arguments import parse_dict_to_sysargs
41
+
41
42
  if action is None:
42
43
  action = []
43
44
  if sysargs is None:
@@ -52,7 +53,6 @@ def api(
52
53
 
53
54
  from meerschaum.config import get_config
54
55
  from meerschaum.connectors import get_connector
55
- requests = attempt_import('requests')
56
56
  if debug:
57
57
  from meerschaum.utils.formatting import pprint
58
58
  api_configs = get_config('meerschaum', 'connectors', 'api', patch=True)
@@ -68,12 +68,13 @@ def api(
68
68
  del action[0]
69
69
  if len(args_to_send) > 1:
70
70
  del args_to_send[0]
71
+
71
72
  kw['action'] = action
72
73
  kw['debug'] = debug
73
74
  kw['sysargs'] = args_to_send
74
75
  kw['yes'] = True
75
76
 
76
- api_conn = get_connector(type='api', label=api_label)
77
+ api_conn = get_connector(f'api:{api_label}')
77
78
 
78
79
  if mrsm_instance is not None and str(mrsm_instance) == str(api_conn):
79
80
  warn(
@@ -83,9 +84,8 @@ def api(
83
84
  elif mrsm_instance is not None:
84
85
  kw['mrsm_instance'] = str(mrsm_instance)
85
86
 
86
- success, message = api_conn.do_action(**kw)
87
- print_tuple((success, message), common_only=True)
88
- msg = f"Action " + ('succeeded' if success else 'failed') + " with message:\n" + str(message)
87
+ sysargs = parse_dict_to_sysargs(kw)
88
+ success, message = api_conn.do_action(sysargs)
89
89
  return success, message
90
90
 
91
91
  def _api_start(
@@ -0,0 +1,95 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Attach to running jobs.
7
+ """
8
+
9
+ import meerschaum as mrsm
10
+ from meerschaum.utils.typing import Optional, List, Any, SuccessTuple
11
+
12
+ def attach(
13
+ action: Optional[List[str]] = None,
14
+ **kwargs: Any
15
+ ) -> SuccessTuple:
16
+ """
17
+ Attach to a job.
18
+ """
19
+ from meerschaum.actions import choose_subaction
20
+ attach_options = {
21
+ 'jobs': _attach_jobs,
22
+ 'logs': _attach_logs,
23
+ }
24
+ return choose_subaction(action, attach_options, **kwargs)
25
+
26
+
27
+ def _complete_attach(
28
+ action: Optional[List[str]] = None,
29
+ **kw: Any
30
+ ) -> List[str]:
31
+ """
32
+ Override the default Meerschaum `complete_` function.
33
+ """
34
+ from meerschaum.actions.start import _complete_start_jobs
35
+
36
+ if action is None:
37
+ action = []
38
+
39
+ options = {
40
+ 'job': _complete_start_jobs,
41
+ 'jobs': _complete_start_jobs,
42
+ 'log': _complete_start_jobs,
43
+ 'logs': _complete_start_jobs,
44
+ }
45
+
46
+ if (
47
+ len(action) > 0 and action[0] in options
48
+ and kw.get('line', '').split(' ')[-1] != action[0]
49
+ ):
50
+ sub = action[0]
51
+ del action[0]
52
+ return options[sub](action=action, **kw)
53
+
54
+ from meerschaum._internal.shell import default_action_completer
55
+ return default_action_completer(action=(['attach'] + action), **kw)
56
+
57
+
58
+ def _attach_jobs(
59
+ action: Optional[List[str]] = None,
60
+ name: Optional[str] = None,
61
+ executor_keys: Optional[str] = None,
62
+ **kwargs: Any
63
+ ) -> SuccessTuple:
64
+ """
65
+ Attach to a job, and prompt the user when blocking on input.
66
+ """
67
+ if not action and not name:
68
+ return False, "Provide the name of the job to attach to."
69
+
70
+ name = name or action[0]
71
+ job = mrsm.Job(name, executor_keys=executor_keys)
72
+ if not job.exists():
73
+ return False, f"Job '{job.name}' does not exist."
74
+
75
+ job.monitor_logs(
76
+ stop_on_exit=True,
77
+ strip_timestamps=True,
78
+ )
79
+
80
+ return True, "Success"
81
+
82
+
83
+ def _attach_logs(*args, **kwargs) -> SuccessTuple:
84
+ """
85
+ Attach to jobs' logs.
86
+ """
87
+ from meerschaum.actions.show import _show_logs
88
+ return _show_logs(*args, **kwargs)
89
+
90
+
91
+ ### NOTE: This must be the final statement of the module.
92
+ ### Any subactions added below these lines will not
93
+ ### be added to the `help` docstring.
94
+ from meerschaum.actions import choices_docstring as _choices_docstring
95
+ attach.__doc__ += _choices_docstring('attach')
@@ -391,6 +391,7 @@ def _complete_delete_connectors(
391
391
 
392
392
  def _delete_jobs(
393
393
  action: Optional[List[str]] = None,
394
+ executor_keys: Optional[str] = None,
394
395
  noask: bool = False,
395
396
  nopretty: bool = False,
396
397
  force: bool = False,
@@ -404,8 +405,12 @@ def _delete_jobs(
404
405
  If the job is running, ask to kill the job first.
405
406
 
406
407
  """
407
- from meerschaum.utils.daemon import (
408
- Daemon, get_running_daemons, get_stopped_daemons, get_filtered_daemons, get_paused_daemons
408
+ from meerschaum.utils.jobs import (
409
+ Job,
410
+ get_running_jobs,
411
+ get_stopped_jobs,
412
+ get_filtered_jobs,
413
+ get_paused_jobs,
409
414
  )
410
415
  from meerschaum.utils.prompt import yes_no
411
416
  from meerschaum.utils.formatting._jobs import pprint_jobs
@@ -413,49 +418,53 @@ def _delete_jobs(
413
418
  from meerschaum.utils.warnings import warn
414
419
  from meerschaum.utils.misc import items_str
415
420
  from meerschaum.actions import actions
416
- daemons = get_filtered_daemons(action, warn=(not nopretty))
417
- if not daemons:
421
+
422
+ jobs = get_filtered_jobs(executor_keys, action, debug=debug)
423
+ if not jobs:
418
424
  return True, "No jobs to delete; nothing to do."
419
425
 
420
426
  _delete_all_jobs = False
421
427
  if not action:
422
428
  if not force:
423
- pprint_jobs(daemons)
429
+ pprint_jobs(jobs)
424
430
  if not yes_no(
425
431
  "Delete all jobs? This cannot be undone!",
426
432
  noask=noask, yes=yes, default='n'
427
433
  ):
428
434
  return False, "No jobs were deleted."
435
+
429
436
  _delete_all_jobs = True
430
- _running_daemons = get_running_daemons(daemons)
431
- _paused_daemons = get_paused_daemons(daemons)
432
- _stopped_daemons = get_stopped_daemons(daemons)
433
- _to_delete = _stopped_daemons
434
437
 
435
- to_stop_daemons = _running_daemons + _paused_daemons
436
- if to_stop_daemons:
438
+ _running_jobs = get_running_jobs(executor_keys, jobs, debug=debug)
439
+ _paused_jobs = get_paused_jobs(executor_keys, jobs, debug=debug)
440
+ _stopped_jobs = get_stopped_jobs(executor_keys, jobs, debug=debug)
441
+ _to_delete = _stopped_jobs
442
+
443
+ to_stop_jobs = {**_running_jobs, **_paused_jobs}
444
+ if to_stop_jobs:
437
445
  clear_screen(debug=debug)
438
446
  if not force:
439
- pprint_jobs(to_stop_daemons, nopretty=nopretty)
447
+ pprint_jobs(to_stop_jobs, nopretty=nopretty)
440
448
  if force or yes_no(
441
449
  "Stop these jobs?",
442
450
  default='n', yes=yes, noask=noask
443
451
  ):
444
452
  actions['stop'](
445
- action = (['jobs'] + [d.daemon_id for d in to_stop_daemons]),
446
- nopretty = nopretty,
447
- yes = yes,
448
- force = force,
449
- noask = noask,
450
- debug = debug,
453
+ action=(['jobs'] + [_name for _name in to_stop_jobs]),
454
+ executor_keys=executor_keys,
455
+ nopretty=nopretty,
456
+ yes=yes,
457
+ force=force,
458
+ noask=noask,
459
+ debug=debug,
451
460
  **kw
452
461
  )
453
462
  ### Ensure the running jobs are dead.
454
- if get_running_daemons(daemons):
463
+ if get_running_jobs(executor_keys, jobs, debug=debug):
455
464
  return False, (
456
465
  f"Failed to kill running jobs. Please stop these jobs before deleting."
457
466
  )
458
- _to_delete += to_stop_daemons
467
+ _to_delete.update(to_stop_jobs)
459
468
 
460
469
  ### User decided not to kill running jobs.
461
470
  else:
@@ -473,17 +482,17 @@ def _delete_jobs(
473
482
  return False, "No jobs were deleted."
474
483
 
475
484
  _deleted = []
476
- for d in _to_delete:
477
- d.cleanup()
478
- if d.path.exists() and not nopretty:
479
- warn(f"Failed to delete job '{d.daemon_id}'.", stack=False)
485
+ for name, job in _to_delete.items():
486
+ delete_success, delete_msg = job.delete()
487
+ if not delete_success:
488
+ warn(f"Failed to delete job '{name}'.", stack=False)
480
489
  continue
481
- _deleted.append(d)
490
+ _deleted.append(name)
482
491
 
483
492
  return (
484
493
  len(_deleted) > 0,
485
494
  ("Deleted job" + ("s" if len(_deleted) != 1 else '')
486
- + f" {items_str([d.daemon_id for d in _deleted])}."),
495
+ + f" {items_str([_name for _name in _deleted])}."),
487
496
  )
488
497
 
489
498
 
@@ -203,14 +203,23 @@ def _register_plugins(
203
203
  successes = {}
204
204
 
205
205
  for name, plugin in plugins_to_register.items():
206
- desc = None
207
206
  plugin.attributes = repo_connector.get_plugin_attributes(plugin, debug=debug)
208
207
  if plugin.attributes is None:
209
208
  plugin.attributes = {}
209
+
210
+ try:
211
+ description_text = plugin.attributes.get('description', '')
212
+ doc_text = plugin.module.__doc__.lstrip().rstrip()
213
+ except Exception:
214
+ description_text = ''
215
+ doc_text = ''
216
+
217
+ desc = description_text or doc_text or ''
218
+
210
219
  question = f"Would you like to add a description to plugin '{name}'?"
211
- if plugin.attributes.get('description', None):
220
+ if desc:
212
221
  info(f"Found existing description for plugin '{plugin}':")
213
- print(plugin.attributes['description'])
222
+ print(desc)
214
223
  question = (
215
224
  "Would you like to overwrite this description?\n"
216
225
  + "To edit the existing text, visit /dash/plugins for this repository."
@@ -220,9 +229,14 @@ def _register_plugins(
220
229
  default='n',
221
230
  yes=yes
222
231
  ):
223
- info('Press (Esc + Enter) to submit the description, (CTRL + C) to cancel.')
232
+ info('Press (Esc + Enter) to submit, (CTRL + C) to cancel.')
224
233
  try:
225
- desc = prompt('', multiline=True, icon=False)
234
+ desc = prompt(
235
+ '',
236
+ multiline=True,
237
+ icon=False,
238
+ default_editable=desc.lstrip().rstrip(),
239
+ )
226
240
  except KeyboardInterrupt:
227
241
  desc = None
228
242
  if desc == '':