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
@@ -9,10 +9,12 @@ Intercept OS-level file descriptors.
9
9
  import os
10
10
  import select
11
11
  import traceback
12
+ import errno
12
13
  from threading import Event
13
14
  from datetime import datetime
14
15
  from meerschaum.utils.typing import Callable
15
16
  from meerschaum.utils.warnings import warn
17
+ from meerschaum.config.paths import DAEMON_ERROR_LOG_PATH
16
18
 
17
19
  FD_CLOSED: int = 9
18
20
  STOP_READING_FD_EVENT: Event = Event()
@@ -65,8 +67,13 @@ class FileDescriptorInterceptor:
65
67
  except BlockingIOError:
66
68
  continue
67
69
  except OSError as e:
68
- from meerschaum.utils.warnings import warn
69
- warn(f"OSError in FileDescriptorInterceptor: {e}")
70
+ if e.errno == errno.EBADF:
71
+ ### File descriptor is closed.
72
+ pass
73
+ elif e.errno == errno.EINTR:
74
+ continue # Interrupted system call, just try again
75
+ else:
76
+ warn(f"OSError in FileDescriptorInterceptor: {e}")
70
77
  break
71
78
 
72
79
  try:
@@ -86,9 +93,11 @@ class FileDescriptorInterceptor:
86
93
  else data.replace(b'\n', b'\n' + injected_bytes)
87
94
  )
88
95
  os.write(self.new_file_descriptor, modified_data)
89
- except Exception as e:
90
- from meerschaum.utils.warnings import warn
91
- warn(f"Error in FileDescriptorInterceptor data processing: {e}")
96
+ except (BrokenPipeError, OSError):
97
+ break
98
+ except Exception:
99
+ with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
100
+ f.write(traceback.format_exc())
92
101
  break
93
102
 
94
103
 
@@ -103,7 +112,7 @@ class FileDescriptorInterceptor:
103
112
  except OSError as e:
104
113
  if e.errno != FD_CLOSED:
105
114
  warn(
106
- f"Error while trying to close the duplicated file descriptor:\n"
115
+ "Error while trying to close the duplicated file descriptor:\n"
107
116
  + f"{traceback.format_exc()}"
108
117
  )
109
118
 
@@ -112,7 +121,7 @@ class FileDescriptorInterceptor:
112
121
  except OSError as e:
113
122
  if e.errno != FD_CLOSED:
114
123
  warn(
115
- f"Error while trying to close the write-pipe "
124
+ "Error while trying to close the write-pipe "
116
125
  + "to the intercepted file descriptor:\n"
117
126
  + f"{traceback.format_exc()}"
118
127
  )
@@ -121,7 +130,7 @@ class FileDescriptorInterceptor:
121
130
  except OSError as e:
122
131
  if e.errno != FD_CLOSED:
123
132
  warn(
124
- f"Error while trying to close the read-pipe "
133
+ "Error while trying to close the read-pipe "
125
134
  + "to the intercepted file descriptor:\n"
126
135
  + f"{traceback.format_exc()}"
127
136
  )
@@ -131,7 +140,7 @@ class FileDescriptorInterceptor:
131
140
  except OSError as e:
132
141
  if e.errno != FD_CLOSED:
133
142
  warn(
134
- f"Error while trying to close the signal-read-pipe "
143
+ "Error while trying to close the signal-read-pipe "
135
144
  + "to the intercepted file descriptor:\n"
136
145
  + f"{traceback.format_exc()}"
137
146
  )
@@ -141,7 +150,7 @@ class FileDescriptorInterceptor:
141
150
  except OSError as e:
142
151
  if e.errno != FD_CLOSED:
143
152
  warn(
144
- f"Error while trying to close the signal-write-pipe "
153
+ "Error while trying to close the signal-write-pipe "
145
154
  + "to the intercepted file descriptor:\n"
146
155
  + f"{traceback.format_exc()}"
147
156
  )
@@ -63,6 +63,9 @@ class RotatingFile(io.IOBase):
63
63
 
64
64
  write_timestamps: bool, default False
65
65
  If `True`, prepend the current UTC timestamp to each line of the file.
66
+
67
+ timestamp_format: str, default '%Y-%m-%d %H:%M'
68
+ If `write_timestamps` is `True`, use this format for the timestamps.
66
69
  """
67
70
  self.file_path = pathlib.Path(file_path)
68
71
  if num_files_to_keep is None:
@@ -232,10 +235,10 @@ class RotatingFile(io.IOBase):
232
235
 
233
236
 
234
237
  def refresh_files(
235
- self,
236
- potential_new_len: int = 0,
237
- start_interception: bool = False,
238
- ) -> '_io.TextUIWrapper':
238
+ self,
239
+ potential_new_len: int = 0,
240
+ start_interception: bool = False,
241
+ ) -> '_io.TextUIWrapper':
239
242
  """
240
243
  Check the state of the subfiles.
241
244
  If the latest subfile is too large, create a new file and delete old ones.
@@ -339,7 +342,7 @@ class RotatingFile(io.IOBase):
339
342
 
340
343
  def get_timestamp_prefix_str(self) -> str:
341
344
  """
342
- Return the current minute prefixm string.
345
+ Return the current minute prefix string.
343
346
  """
344
347
  return datetime.now(timezone.utc).strftime(self.timestamp_format) + ' | '
345
348
 
@@ -371,6 +374,9 @@ class RotatingFile(io.IOBase):
371
374
  self._current_file_obj.write(data)
372
375
  if suffix_str:
373
376
  self._current_file_obj.write(suffix_str)
377
+ except BrokenPipeError:
378
+ warn("BrokenPipeError encountered. The daemon may have been terminated.")
379
+ return
374
380
  except Exception as e:
375
381
  warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
376
382
  self.flush()
@@ -565,7 +571,8 @@ class RotatingFile(io.IOBase):
565
571
  return
566
572
 
567
573
  self._cursor = (max_ix, position)
568
- self._current_file_obj.seek(position)
574
+ if self._current_file_obj is not None:
575
+ self._current_file_obj.seek(position)
569
576
 
570
577
 
571
578
  def flush(self) -> None:
@@ -581,10 +588,14 @@ class RotatingFile(io.IOBase):
581
588
  if self.redirect_streams:
582
589
  try:
583
590
  sys.stdout.flush()
591
+ except BrokenPipeError:
592
+ pass
584
593
  except Exception as e:
585
594
  warn(f"Failed to flush STDOUT:\n{traceback.format_exc()}")
586
595
  try:
587
596
  sys.stderr.flush()
597
+ except BrokenPipeError:
598
+ pass
588
599
  except Exception as e:
589
600
  warn(f"Failed to flush STDERR:\n{traceback.format_exc()}")
590
601
 
@@ -596,7 +607,6 @@ class RotatingFile(io.IOBase):
596
607
  if not self.write_timestamps:
597
608
  return
598
609
 
599
- threads = self.__dict__.get('_interceptor_threads', [])
600
610
  self._stdout_interceptor = FileDescriptorInterceptor(
601
611
  sys.stdout.fileno(),
602
612
  self.get_timestamp_prefix_str,
@@ -639,6 +649,7 @@ class RotatingFile(io.IOBase):
639
649
  """
640
650
  if not self.write_timestamps:
641
651
  return
652
+
642
653
  interceptors = self.__dict__.get('_interceptors', [])
643
654
  interceptor_threads = self.__dict__.get('_interceptor_threads', [])
644
655
 
@@ -0,0 +1,110 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Create a file manager to pass STDIN to the Daemon.
7
+ """
8
+
9
+ import io
10
+ import pathlib
11
+ import time
12
+ import os
13
+ import selectors
14
+ import traceback
15
+
16
+ from meerschaum.utils.typing import Optional
17
+ from meerschaum.utils.warnings import warn
18
+
19
+
20
+ class StdinFile(io.TextIOBase):
21
+ """
22
+ Redirect user input into a Daemon's context.
23
+ """
24
+ def __init__(
25
+ self,
26
+ file_path: pathlib.Path,
27
+ lock_file_path: Optional[pathlib.Path] = None,
28
+ ):
29
+ self.file_path = file_path
30
+ self.blocking_file_path = (
31
+ lock_file_path
32
+ if lock_file_path is not None
33
+ else (file_path.parent / (file_path.name + '.block'))
34
+ )
35
+ self._file_handler = None
36
+ self._fd = None
37
+ self.sel = selectors.DefaultSelector()
38
+
39
+ @property
40
+ def file_handler(self):
41
+ """
42
+ Return the read file handler to the provided file path.
43
+ """
44
+ if self._file_handler is not None:
45
+ return self._file_handler
46
+
47
+ if self.file_path.exists():
48
+ self.file_path.unlink()
49
+
50
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
51
+
52
+ self._fd = os.open(self.file_path, os.O_RDONLY | os.O_NONBLOCK)
53
+ self._file_handler = os.fdopen(self._fd, 'rb', buffering=0)
54
+ self.sel.register(self._file_handler, selectors.EVENT_READ)
55
+ return self._file_handler
56
+
57
+ def write(self, data):
58
+ if isinstance(data, str):
59
+ data = data.encode('utf-8')
60
+
61
+ with open(self.file_path, 'wb') as f:
62
+ f.write(data)
63
+
64
+ def fileno(self):
65
+ fileno = self.file_handler.fileno()
66
+ return fileno
67
+
68
+ def read(self, size=-1):
69
+ """
70
+ Read from the FIFO pipe, blocking on EOFError.
71
+ """
72
+ _ = self.file_handler
73
+ while True:
74
+ try:
75
+ data = self._file_handler.read(size)
76
+ if data:
77
+ try:
78
+ if self.blocking_file_path.exists():
79
+ self.blocking_file_path.unlink()
80
+ except Exception:
81
+ warn(traceback.format_exc())
82
+ return data.decode('utf-8')
83
+ except (OSError, EOFError):
84
+ pass
85
+
86
+ self.blocking_file_path.touch()
87
+ time.sleep(0.1)
88
+
89
+ def readline(self, size=-1):
90
+ line = ''
91
+ while True:
92
+ data = self.read(1)
93
+ if not data or data == '\n':
94
+ break
95
+ line += data
96
+
97
+ return line
98
+
99
+ def close(self):
100
+ if self._file_handler is not None:
101
+ self.sel.unregister(self._file_handler)
102
+ self._file_handler.close()
103
+ os.close(self._fd)
104
+ self._file_handler = None
105
+ self._fd = None
106
+
107
+ super().close()
108
+
109
+ def is_open(self):
110
+ return self._file_handler is not None
@@ -67,19 +67,17 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
67
67
  if daemon.status == 'running':
68
68
  return True, f"Daemon '{daemon}' is already running."
69
69
  return daemon.run(
70
- debug = debug,
71
- allow_dirty_run = True,
70
+ debug=debug,
71
+ allow_dirty_run=True,
72
72
  )
73
73
 
74
74
  success_tuple = run_daemon(
75
75
  entry,
76
76
  filtered_sysargs,
77
- daemon_id = _args.get('name', None) if _args else None,
78
- label = label,
79
- keep_daemon_output = ('--rm' not in sysargs)
77
+ daemon_id=_args.get('name', None) if _args else None,
78
+ label=label,
79
+ keep_daemon_output=('--rm' not in (sysargs or [])),
80
80
  )
81
- if not isinstance(success_tuple, tuple):
82
- success_tuple = False, str(success_tuple)
83
81
  return success_tuple
84
82
 
85
83
 
@@ -109,25 +107,25 @@ def daemon_action(**kw) -> SuccessTuple:
109
107
 
110
108
 
111
109
  def run_daemon(
112
- func: Callable[[Any], Any],
113
- *args,
114
- daemon_id: Optional[str] = None,
115
- keep_daemon_output: bool = False,
116
- allow_dirty_run: bool = False,
117
- label: Optional[str] = None,
118
- **kw
119
- ) -> Any:
110
+ func: Callable[[Any], Any],
111
+ *args,
112
+ daemon_id: Optional[str] = None,
113
+ keep_daemon_output: bool = True,
114
+ allow_dirty_run: bool = False,
115
+ label: Optional[str] = None,
116
+ **kw
117
+ ) -> Any:
120
118
  """Execute a function as a daemon."""
121
119
  daemon = Daemon(
122
120
  func,
123
- daemon_id = daemon_id,
124
- target_args = [arg for arg in args],
125
- target_kw = kw,
126
- label = label,
121
+ daemon_id=daemon_id,
122
+ target_args=[arg for arg in args],
123
+ target_kw=kw,
124
+ label=label,
127
125
  )
128
126
  return daemon.run(
129
- keep_daemon_output = keep_daemon_output,
130
- allow_dirty_run = allow_dirty_run,
127
+ keep_daemon_output=keep_daemon_output,
128
+ allow_dirty_run=allow_dirty_run,
131
129
  )
132
130
 
133
131
 
@@ -183,7 +181,11 @@ def get_daemon_ids() -> List[str]:
183
181
  """
184
182
  Return the IDs of all daemons on disk.
185
183
  """
186
- return sorted(os.listdir(DAEMON_RESOURCES_PATH))
184
+ return [
185
+ daemon_dir
186
+ for daemon_dir in sorted(os.listdir(DAEMON_RESOURCES_PATH))
187
+ if (DAEMON_RESOURCES_PATH / daemon_dir / 'properties.json').exists()
188
+ ]
187
189
 
188
190
 
189
191
  def get_running_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
@@ -227,10 +229,11 @@ def get_stopped_daemons(daemons: Optional[List[Daemon]] = None) -> List[Daemon]:
227
229
 
228
230
 
229
231
  def get_filtered_daemons(
230
- filter_list: Optional[List[str]] = None,
231
- warn: bool = False,
232
- ) -> List[Daemon]:
233
- """Return a list of `Daemons` filtered by a list of `daemon_ids`.
232
+ filter_list: Optional[List[str]] = None,
233
+ warn: bool = False,
234
+ ) -> List[Daemon]:
235
+ """
236
+ Return a list of `Daemons` filtered by a list of `daemon_ids`.
234
237
  Only `Daemons` that exist are returned.
235
238
 
236
239
  If `filter_list` is `None` or empty, return all `Daemons` (from `get_daemons()`).
@@ -252,13 +255,14 @@ def get_filtered_daemons(
252
255
  if not filter_list:
253
256
  daemons = get_daemons()
254
257
  return [d for d in daemons if not d.hidden]
258
+
255
259
  from meerschaum.utils.warnings import warn as _warn
256
260
  daemons = []
257
261
  for d_id in filter_list:
258
262
  try:
259
263
  d = Daemon(daemon_id=d_id)
260
264
  _exists = d.path.exists()
261
- except Exception as e:
265
+ except Exception:
262
266
  _exists = False
263
267
  if not _exists:
264
268
  if warn:
@@ -268,3 +272,12 @@ def get_filtered_daemons(
268
272
  pass
269
273
  daemons.append(d)
270
274
  return daemons
275
+
276
+
277
+ def running_in_daemon() -> bool:
278
+ """
279
+ Return whether the current thread is running in a Daemon context.
280
+ """
281
+ from meerschaum.config.static import STATIC_CONFIG
282
+ daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
283
+ return daemon_env_var in os.environ
@@ -10,6 +10,7 @@ from __future__ import annotations
10
10
  import platform
11
11
  import os
12
12
  import sys
13
+ import meerschaum as mrsm
13
14
  from meerschaum.utils.typing import Optional, Union, Any, Dict
14
15
  from meerschaum.utils.formatting._shell import make_header
15
16
  from meerschaum.utils.formatting._pprint import pprint
@@ -27,12 +28,14 @@ _attrs = {
27
28
  'ANSI': None,
28
29
  'UNICODE': None,
29
30
  'CHARSET': None,
31
+ 'RESET': '\033[0m',
30
32
  }
31
33
  __all__ = sorted([
32
- 'ANSI', 'CHARSET', 'UNICODE',
34
+ 'ANSI', 'CHARSET', 'UNICODE', 'RESET',
33
35
  'colored',
34
36
  'translate_rich_to_termcolor',
35
37
  'get_console',
38
+ 'format_success_tuple',
36
39
  'print_tuple',
37
40
  'print_options',
38
41
  'fill_ansi',
@@ -222,16 +225,17 @@ def get_console():
222
225
 
223
226
 
224
227
  def print_tuple(
225
- tup: tuple,
226
- skip_common: bool = True,
227
- common_only: bool = False,
228
- upper_padding: int = 0,
229
- lower_padding: int = 0,
230
- calm: bool = False,
231
- _progress: Optional['rich.progress.Progress'] = None,
232
- ) -> None:
228
+ tup: mrsm.SuccessTuple,
229
+ skip_common: bool = True,
230
+ common_only: bool = False,
231
+ upper_padding: int = 0,
232
+ lower_padding: int = 0,
233
+ left_padding: int = 1,
234
+ calm: bool = False,
235
+ _progress: Optional['rich.progress.Progress'] = None,
236
+ ) -> None:
233
237
  """
234
- Print `meerschaum.utils.typing.SuccessTuple`.
238
+ Format `meerschaum.utils.typing.SuccessTuple`.
235
239
 
236
240
  Parameters
237
241
  ----------
@@ -247,24 +251,18 @@ def print_tuple(
247
251
  lower_padding: int, default 0
248
252
  How many newlines to append to the message.
249
253
 
254
+ left_padding: int, default 1
255
+ How mant spaces to preprend to the message.
256
+
250
257
  calm: bool, default False
251
258
  If `True`, use the default emoji and color scheme.
259
+
252
260
  """
253
261
  from meerschaum.config.static import STATIC_CONFIG
254
- _init()
255
- try:
256
- status = 'success' if tup[0] else 'failure'
257
- except TypeError:
258
- status = 'failure'
259
- tup = None, None
260
-
261
- if calm:
262
- status += '_calm'
262
+ do_print = True
263
263
 
264
264
  omit_messages = STATIC_CONFIG['system']['success']['ignore']
265
265
 
266
- do_print = True
267
-
268
266
  if common_only:
269
267
  skip_common = False
270
268
  do_print = tup[1] in omit_messages
@@ -272,22 +270,70 @@ def print_tuple(
272
270
  if skip_common:
273
271
  do_print = tup[1] not in omit_messages
274
272
 
275
- if do_print:
276
- ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
277
- from meerschaum.config import get_config
278
- status_config = get_config('formatting', status, patch=True)
279
-
280
- msg = ' ' + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
281
- lines = msg.split('\n')
282
- lines = [lines[0]] + [
283
- ((' ' + line if not line.startswith(' ') else line))
284
- for line in lines[1:]
285
- ]
286
- if ANSI:
287
- lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
288
- msg = '\n'.join(lines)
289
- msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
290
- print(msg)
273
+ if not do_print:
274
+ return
275
+
276
+ print(format_success_tuple(
277
+ tup,
278
+ upper_padding=upper_padding,
279
+ lower_padding=lower_padding,
280
+ calm=calm,
281
+ _progress=_progress,
282
+ ))
283
+
284
+
285
+ def format_success_tuple(
286
+ tup: mrsm.SuccessTuple,
287
+ upper_padding: int = 0,
288
+ lower_padding: int = 0,
289
+ left_padding: int = 1,
290
+ calm: bool = False,
291
+ _progress: Optional['rich.progress.Progress'] = None,
292
+ ) -> str:
293
+ """
294
+ Format `meerschaum.utils.typing.SuccessTuple`.
295
+
296
+ Parameters
297
+ ----------
298
+ upper_padding: int, default 0
299
+ How many newlines to prepend to the message.
300
+
301
+ lower_padding: int, default 0
302
+ How many newlines to append to the message.
303
+
304
+ left_padding: int, default 1
305
+ How mant spaces to preprend to the message.
306
+
307
+ calm: bool, default False
308
+ If `True`, use the default emoji and color scheme.
309
+ """
310
+ from meerschaum.config.static import STATIC_CONFIG
311
+ _init()
312
+ try:
313
+ status = 'success' if tup[0] else 'failure'
314
+ except TypeError:
315
+ status = 'failure'
316
+ tup = None, None
317
+
318
+ if calm:
319
+ status += '_calm'
320
+
321
+ ANSI, CHARSET = __getattr__('ANSI'), __getattr__('CHARSET')
322
+ from meerschaum.config import get_config
323
+ status_config = get_config('formatting', status, patch=True)
324
+
325
+ msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
326
+ lines = msg.split('\n')
327
+ lines = [lines[0]] + [
328
+ ((' ' + line if not line.startswith(' ') else line))
329
+ for line in lines[1:]
330
+ ]
331
+ if ANSI:
332
+ lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
333
+
334
+ msg = '\n'.join(lines)
335
+ msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
336
+ return msg
291
337
 
292
338
 
293
339
  def print_options(