meerschaum 2.0.0rc7__py3-none-any.whl → 2.0.0rc8__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 (44) hide show
  1. meerschaum/actions/__init__.py +97 -48
  2. meerschaum/actions/bootstrap.py +1 -1
  3. meerschaum/actions/clear.py +1 -1
  4. meerschaum/actions/deduplicate.py +1 -1
  5. meerschaum/actions/delete.py +8 -7
  6. meerschaum/actions/drop.py +1 -10
  7. meerschaum/actions/edit.py +1 -1
  8. meerschaum/actions/install.py +1 -1
  9. meerschaum/actions/pause.py +1 -1
  10. meerschaum/actions/register.py +1 -1
  11. meerschaum/actions/setup.py +1 -1
  12. meerschaum/actions/show.py +1 -1
  13. meerschaum/actions/start.py +18 -7
  14. meerschaum/actions/stop.py +5 -4
  15. meerschaum/actions/sync.py +3 -1
  16. meerschaum/actions/uninstall.py +1 -1
  17. meerschaum/actions/upgrade.py +1 -1
  18. meerschaum/actions/verify.py +54 -3
  19. meerschaum/config/_formatting.py +26 -0
  20. meerschaum/config/_jobs.py +28 -5
  21. meerschaum/config/_paths.py +21 -5
  22. meerschaum/config/_version.py +1 -1
  23. meerschaum/connectors/api/_fetch.py +1 -1
  24. meerschaum/connectors/api/_pipes.py +6 -11
  25. meerschaum/connectors/sql/_fetch.py +29 -11
  26. meerschaum/core/Pipe/_deduplicate.py +39 -23
  27. meerschaum/core/Pipe/_dtypes.py +2 -1
  28. meerschaum/core/Pipe/_verify.py +59 -24
  29. meerschaum/plugins/__init__.py +3 -0
  30. meerschaum/utils/daemon/Daemon.py +108 -27
  31. meerschaum/utils/daemon/__init__.py +35 -1
  32. meerschaum/utils/formatting/__init__.py +144 -1
  33. meerschaum/utils/formatting/_pipes.py +28 -5
  34. meerschaum/utils/misc.py +183 -187
  35. meerschaum/utils/packages/__init__.py +1 -1
  36. meerschaum/utils/packages/_packages.py +1 -0
  37. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/METADATA +4 -1
  38. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/RECORD +44 -44
  39. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/LICENSE +0 -0
  40. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/NOTICE +0 -0
  41. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/WHEEL +0 -0
  42. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/entry_points.txt +0 -0
  43. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/top_level.txt +0 -0
  44. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc8.dist-info}/zip-safe +0 -0
@@ -168,7 +168,7 @@ class Daemon:
168
168
  finally:
169
169
  self._log_refresh_timer.cancel()
170
170
  self.rotating_log.close()
171
- if self.pid_path.exists():
171
+ if self.pid is None and self.pid_path.exists():
172
172
  self.pid_path.unlink()
173
173
 
174
174
  if keep_daemon_output:
@@ -254,7 +254,7 @@ class Daemon:
254
254
  return _launch_success_bool, msg
255
255
 
256
256
 
257
- def kill(self, timeout: Optional[int] = 3) -> SuccessTuple:
257
+ def kill(self, timeout: Optional[int] = 8) -> SuccessTuple:
258
258
  """Forcibly terminate a running daemon.
259
259
  Sends a SIGTERM signal to the process.
260
260
 
@@ -293,7 +293,7 @@ class Daemon:
293
293
  return True, "Success"
294
294
 
295
295
 
296
- def quit(self, timeout: Optional[int] = 3) -> SuccessTuple:
296
+ def quit(self, timeout: Union[int, float, None] = None) -> SuccessTuple:
297
297
  """Gracefully quit a running daemon."""
298
298
  if self.status == 'paused':
299
299
  return self.kill(timeout)
@@ -305,10 +305,24 @@ class Daemon:
305
305
 
306
306
  def pause(
307
307
  self,
308
- timeout: Optional[int] = 3,
309
- check_timeout_interval: float = 0.1,
308
+ timeout: Union[int, float, None] = None,
309
+ check_timeout_interval: Union[float, int, None] = None,
310
310
  ) -> SuccessTuple:
311
- """Pause the daemon if it is running."""
311
+ """
312
+ Pause the daemon if it is running.
313
+
314
+ Parameters
315
+ ----------
316
+ timeout: Union[float, int, None], default None
317
+ The maximum number of seconds to wait for a process to suspend.
318
+
319
+ check_timeout_interval: Union[float, int, None], default None
320
+ The number of seconds to wait between checking if the process is still running.
321
+
322
+ Returns
323
+ -------
324
+ A `SuccessTuple` indicating whether the `Daemon` process was successfully suspended.
325
+ """
312
326
  if self.process is None:
313
327
  return False, f"Daemon '{self.daemon_id}' is not running and cannot be paused."
314
328
 
@@ -320,8 +334,18 @@ class Daemon:
320
334
  except Exception as e:
321
335
  return False, f"Failed to pause daemon '{self.daemon_id}':\n{e}"
322
336
 
323
- if timeout is None:
324
- success = self.process.status() == 'stopped'
337
+ timeout = self.get_timeout_seconds(timeout)
338
+ check_timeout_interval = self.get_check_timeout_interval_seconds(
339
+ check_timeout_interval
340
+ )
341
+
342
+ psutil = attempt_import('psutil')
343
+
344
+ if not timeout:
345
+ try:
346
+ success = self.process.status() == 'stopped'
347
+ except psutil.NoSuchProcess as e:
348
+ success = True
325
349
  msg = "Success" if success else f"Failed to suspend daemon '{self.daemon_id}'."
326
350
  if success:
327
351
  self._capture_process_timestamp('paused')
@@ -329,9 +353,12 @@ class Daemon:
329
353
 
330
354
  begin = time.perf_counter()
331
355
  while (time.perf_counter() - begin) < timeout:
332
- if self.process.status() == 'stopped':
333
- self._capture_process_timestamp('paused')
334
- return True, "Success"
356
+ try:
357
+ if self.process.status() == 'stopped':
358
+ self._capture_process_timestamp('paused')
359
+ return True, "Success"
360
+ except psutil.NoSuchProcess as e:
361
+ return False, f"Process exited unexpectedly. Was it killed?\n{e}"
335
362
  time.sleep(check_timeout_interval)
336
363
 
337
364
  return False, (
@@ -342,10 +369,24 @@ class Daemon:
342
369
 
343
370
  def resume(
344
371
  self,
345
- timeout: Optional[int] = 3,
346
- check_timeout_interval: float = 0.1,
372
+ timeout: Union[int, float, None] = None,
373
+ check_timeout_interval: Union[float, int, None] = None,
347
374
  ) -> SuccessTuple:
348
- """Resume the daemon if it is paused."""
375
+ """
376
+ Resume the daemon if it is paused.
377
+
378
+ Parameters
379
+ ----------
380
+ timeout: Union[float, int, None], default None
381
+ The maximum number of seconds to wait for a process to resume.
382
+
383
+ check_timeout_interval: Union[float, int, None], default None
384
+ The number of seconds to wait between checking if the process is still stopped.
385
+
386
+ Returns
387
+ -------
388
+ A `SuccessTuple` indicating whether the `Daemon` process was successfully resumed.
389
+ """
349
390
  if self.status == 'running':
350
391
  return True, f"Daemon '{self.daemon_id}' is already running."
351
392
 
@@ -357,7 +398,12 @@ class Daemon:
357
398
  except Exception as e:
358
399
  return False, f"Failed to resume daemon '{self.daemon_id}':\n{e}"
359
400
 
360
- if timeout is None:
401
+ timeout = self.get_timeout_seconds(timeout)
402
+ check_timeout_interval = self.get_check_timeout_interval_seconds(
403
+ check_timeout_interval
404
+ )
405
+
406
+ if not timeout:
361
407
  success = self.status == 'running'
362
408
  msg = "Success" if success else f"Failed to resume daemon '{self.daemon_id}'."
363
409
  if success:
@@ -417,8 +463,8 @@ class Daemon:
417
463
  def _send_signal(
418
464
  self,
419
465
  signal_to_send,
420
- timeout: Optional[Union[float, int]] = 3,
421
- check_timeout_interval: float = 0.1,
466
+ timeout: Union[float, int, None] = None,
467
+ check_timeout_interval: Union[float, int, None] = None,
422
468
  ) -> SuccessTuple:
423
469
  """Send a signal to the daemon process.
424
470
 
@@ -427,13 +473,11 @@ class Daemon:
427
473
  signal_to_send:
428
474
  The signal the send to the daemon, e.g. `signals.SIGINT`.
429
475
 
430
- timeout:
476
+ timeout: Union[float, int, None], default None
431
477
  The maximum number of seconds to wait for a process to terminate.
432
- Defaults to 3.
433
478
 
434
- check_timeout_interval: float, default 0.1
479
+ check_timeout_interval: Union[float, int, None], default None
435
480
  The number of seconds to wait between checking if the process is still running.
436
- Defaults to 0.1.
437
481
 
438
482
  Returns
439
483
  -------
@@ -444,11 +488,17 @@ class Daemon:
444
488
  except Exception as e:
445
489
  return False, f"Failed to send signal {signal_to_send}:\n{traceback.format_exc()}"
446
490
 
447
- if timeout is None:
491
+ timeout = self.get_timeout_seconds(timeout)
492
+ check_timeout_interval = self.get_check_timeout_interval_seconds(
493
+ check_timeout_interval
494
+ )
495
+
496
+ if not timeout:
448
497
  return True, f"Successfully sent '{signal}' to daemon '{self.daemon_id}'."
498
+
449
499
  begin = time.perf_counter()
450
500
  while (time.perf_counter() - begin) < timeout:
451
- if not self.pid_path.exists():
501
+ if not self.status == 'running':
452
502
  return True, "Success"
453
503
  time.sleep(check_timeout_interval)
454
504
 
@@ -464,8 +514,8 @@ class Daemon:
464
514
  raise a `FileExistsError`.
465
515
  """
466
516
  try:
467
- self.path.mkdir(parents=True, exist_ok=False)
468
- _already_exists = False
517
+ self.path.mkdir(parents=True, exist_ok=True)
518
+ _already_exists = any(os.scandir(self.path))
469
519
  except FileExistsError:
470
520
  _already_exists = True
471
521
 
@@ -506,8 +556,17 @@ class Daemon:
506
556
  if self.process is None:
507
557
  return 'stopped'
508
558
 
509
- if self.process.status() == 'stopped':
510
- return 'paused'
559
+ psutil = attempt_import('psutil')
560
+ try:
561
+ if self.process.status() == 'stopped':
562
+ return 'paused'
563
+ except psutil.NoSuchProcess:
564
+ if self.pid_path.exists():
565
+ try:
566
+ self.pid_path.unlink()
567
+ except Exception as e:
568
+ pass
569
+ return 'stopped'
511
570
 
512
571
  return 'running'
513
572
 
@@ -804,6 +863,28 @@ class Daemon:
804
863
  self.rotating_log.delete()
805
864
 
806
865
 
866
+ def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]:
867
+ """
868
+ Return the timeout value to use. Use `--timeout-seconds` if provided,
869
+ else the configured default (8).
870
+ """
871
+ if isinstance(timeout, (int, float)):
872
+ return timeout
873
+ return get_config('jobs', 'timeout_seconds')
874
+
875
+
876
+ def get_check_timeout_interval_seconds(
877
+ self,
878
+ check_timeout_interval: Union[int, float, None] = None,
879
+ ) -> Union[int, float]:
880
+ """
881
+ Return the interval value to check the status of timeouts.
882
+ """
883
+ if isinstance(check_timeout_interval, (int, float)):
884
+ return check_timeout_interval
885
+ return get_config('jobs', 'check_timeout_interval_seconds')
886
+
887
+
807
888
  def __getstate__(self):
808
889
  """
809
890
  Pickle this Daemon.
@@ -14,6 +14,7 @@ from meerschaum.utils.daemon.Daemon import Daemon
14
14
  from meerschaum.utils.daemon.Log import Log
15
15
  from meerschaum.utils.daemon.RotatingFile import RotatingFile
16
16
 
17
+
17
18
  def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
18
19
  """Parse sysargs and execute a Meerschaum action as a daemon.
19
20
 
@@ -27,7 +28,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
27
28
  A SuccessTuple.
28
29
  """
29
30
  from meerschaum._internal.entry import entry
30
- _args = None
31
+ _args = {}
31
32
  if '--name' in sysargs or '--job-name' in sysargs:
32
33
  from meerschaum._internal.arguments._parse_arguments import parse_arguments
33
34
  _args = parse_arguments(sysargs)
@@ -36,6 +37,38 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
36
37
  label = shlex.join(filtered_sysargs) if sysargs else None
37
38
  except Exception as e:
38
39
  label = ' '.join(filtered_sysargs) if sysargs else None
40
+
41
+ name = _args.get('name', None)
42
+ daemon = None
43
+ if name:
44
+ try:
45
+ daemon = Daemon(daemon_id=name)
46
+ except Exception as e:
47
+ daemon = None
48
+
49
+ if daemon is not None:
50
+ existing_sysargs = daemon.properties['target']['args'][0]
51
+ existing_kwargs = parse_arguments(existing_sysargs)
52
+
53
+ ### Remove sysargs because flags are aliased.
54
+ _ = _args.pop('daemon', None)
55
+ _ = _args.pop('sysargs', None)
56
+ _ = _args.pop('filtered_sysargs', None)
57
+ debug = _args.pop('debug', None)
58
+ _args['sub_args'] = sorted(_args.get('sub_args', []))
59
+ _ = existing_kwargs.pop('daemon', None)
60
+ _ = existing_kwargs.pop('sysargs', None)
61
+ _ = existing_kwargs.pop('filtered_sysargs', None)
62
+ _ = existing_kwargs.pop('debug', None)
63
+ existing_kwargs['sub_args'] = sorted(existing_kwargs.get('sub_args', []))
64
+
65
+ ### Only run if the kwargs equal or no actions are provided.
66
+ if existing_kwargs == _args or not _args.get('action', []):
67
+ return daemon.run(
68
+ debug = debug,
69
+ allow_dirty_run = True,
70
+ )
71
+
39
72
  success_tuple = run_daemon(
40
73
  entry,
41
74
  filtered_sysargs,
@@ -47,6 +80,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
47
80
  success_tuple = False, str(success_tuple)
48
81
  return success_tuple
49
82
 
83
+
50
84
  def daemon_action(**kw) -> SuccessTuple:
51
85
  """Execute a Meerschaum action as a daemon."""
52
86
  from meerschaum.utils.packages import run_python_package
@@ -33,6 +33,7 @@ __all__ = sorted([
33
33
  'translate_rich_to_termcolor',
34
34
  'get_console',
35
35
  'print_tuple',
36
+ 'print_options',
36
37
  'fill_ansi',
37
38
  'pprint',
38
39
  'highlight_pipes',
@@ -222,9 +223,29 @@ def print_tuple(
222
223
  common_only: bool = False,
223
224
  upper_padding: int = 0,
224
225
  lower_padding: int = 0,
226
+ calm: bool = False,
225
227
  _progress: Optional['rich.progress.Progress'] = None,
226
228
  ) -> None:
227
- """Print `meerschaum.utils.typing.SuccessTuple`."""
229
+ """
230
+ Print `meerschaum.utils.typing.SuccessTuple`.
231
+
232
+ Parameters
233
+ ----------
234
+ skip_common: bool, default True
235
+ If `True`, do not print common success tuples (i.e. `(True, "Success")`).
236
+
237
+ common_only: bool, default False
238
+ If `True`, only print if the success tuple is common.
239
+
240
+ upper_padding: int, default 0
241
+ How many newlines to prepend to the message.
242
+
243
+ lower_padding: int, default 0
244
+ How many newlines to append to the message.
245
+
246
+ calm: bool, default False
247
+ If `True`, use the default emoji and color scheme.
248
+ """
228
249
  from meerschaum.config.static import STATIC_CONFIG
229
250
  _init()
230
251
  try:
@@ -233,6 +254,9 @@ def print_tuple(
233
254
  status = 'failure'
234
255
  tup = None, None
235
256
 
257
+ if calm:
258
+ status += '_calm'
259
+
236
260
  omit_messages = STATIC_CONFIG['system']['success']['ignore']
237
261
 
238
262
  do_print = True
@@ -262,6 +286,125 @@ def print_tuple(
262
286
  print(msg)
263
287
 
264
288
 
289
+ def print_options(
290
+ options: Optional[Dict[str, Any]] = None,
291
+ nopretty: bool = False,
292
+ no_rich: bool = False,
293
+ name: str = 'options',
294
+ header: Optional[str] = None,
295
+ num_cols: Optional[int] = None,
296
+ adjust_cols: bool = True,
297
+ **kw
298
+ ) -> None:
299
+ """
300
+ Print items in an iterable as a fancy table.
301
+
302
+ Parameters
303
+ ----------
304
+ options: Optional[Dict[str, Any]], default None
305
+ The iterable to be printed.
306
+
307
+ nopretty: bool, default False
308
+ If `True`, don't use fancy formatting.
309
+
310
+ no_rich: bool, default False
311
+ If `True`, don't use `rich` to format the output.
312
+
313
+ name: str, default 'options'
314
+ The text in the default header after `'Available'`.
315
+
316
+ header: Optional[str], default None
317
+ If provided, override `name` and use this as the header text.
318
+
319
+ num_cols: Optional[int], default None
320
+ How many columns in the table. Depends on the terminal size. If `None`, use 8.
321
+
322
+ adjust_cols: bool, default True
323
+ If `True`, adjust the number of columns depending on the terminal size.
324
+
325
+ """
326
+ import os
327
+ from meerschaum.utils.packages import import_rich
328
+ from meerschaum.utils.formatting import make_header, highlight_pipes
329
+ from meerschaum.actions import actions as _actions
330
+ from meerschaum.utils.misc import get_cols_lines, string_width, iterate_chunks
331
+
332
+
333
+ if options is None:
334
+ options = {}
335
+ _options = []
336
+ for o in options:
337
+ _options.append(str(o))
338
+ _header = f"Available {name}" if header is None else header
339
+
340
+ if num_cols is None:
341
+ num_cols = 8
342
+
343
+ def _print_options_no_rich():
344
+ if not nopretty:
345
+ print()
346
+ print(make_header(_header))
347
+ ### print actions
348
+ for option in sorted(_options):
349
+ if not nopretty:
350
+ print(" - ", end="")
351
+ print(option)
352
+ if not nopretty:
353
+ print()
354
+
355
+ rich = import_rich()
356
+ if rich is None or nopretty or no_rich:
357
+ _print_options_no_rich()
358
+ return None
359
+
360
+ ### Prevent too many options from being truncated on small terminals.
361
+ if adjust_cols and _options:
362
+ _cols, _lines = get_cols_lines()
363
+ while num_cols > 1:
364
+ cell_len = int(((_cols - 4) - (3 * (num_cols - 1))) / num_cols)
365
+ num_too_big = sum([(1 if string_width(o) > cell_len else 0) for o in _options])
366
+ if num_too_big > int(len(_options) / 3):
367
+ num_cols -= 1
368
+ continue
369
+ break
370
+
371
+ from meerschaum.utils.formatting import pprint, get_console
372
+ from meerschaum.utils.packages import attempt_import
373
+ rich_columns = attempt_import('rich.columns')
374
+ rich_panel = attempt_import('rich.panel')
375
+ rich_table = attempt_import('rich.table')
376
+ Text = attempt_import('rich.text').Text
377
+ box = attempt_import('rich.box')
378
+ Panel = rich_panel.Panel
379
+ Columns = rich_columns.Columns
380
+ Table = rich_table.Table
381
+
382
+ if _header is not None:
383
+ table = Table(
384
+ title = '\n' + _header,
385
+ box = box.SIMPLE,
386
+ show_header = False,
387
+ show_footer = False,
388
+ title_style = '',
389
+ expand = True,
390
+ )
391
+ else:
392
+ table = Table.grid(padding=(0, 2))
393
+ for i in range(num_cols):
394
+ table.add_column()
395
+
396
+ chunks = iterate_chunks(
397
+ [Text.from_ansi(highlight_pipes(o)) for o in sorted(_options)],
398
+ num_cols,
399
+ fillvalue=''
400
+ )
401
+ for c in chunks:
402
+ table.add_row(*c)
403
+
404
+ get_console().print(table)
405
+ return None
406
+
407
+
265
408
  def fill_ansi(string: str, style: str = '') -> str:
266
409
  """
267
410
  Fill in non-formatted segments of ANSI text.
@@ -309,19 +309,42 @@ def highlight_pipes(message: str) -> str:
309
309
  """
310
310
  Add syntax highlighting to an info message containing stringified `meerschaum.Pipe` objects.
311
311
  """
312
- if 'Pipe(' not in message or ')' not in message:
312
+ if 'Pipe(' not in message and ')' not in message:
313
313
  return message
314
+
314
315
  from meerschaum import Pipe
315
316
  segments = message.split('Pipe(')
316
317
  msg = ''
317
318
  _d = {}
318
319
  for i, segment in enumerate(segments):
319
- if ',' in segment and ')' in segment:
320
- paren_index = segment.find(')') + 1
321
- code = "_d['pipe'] = Pipe(" + segment[:paren_index]
320
+ comma_index = segment.find(',')
321
+ paren_index = segment.find(')')
322
+ single_quote_index = segment.find("'")
323
+ double_quote_index = segment.find('"')
324
+
325
+ has_comma = comma_index != -1
326
+ has_paren = paren_index != -1
327
+ has_single_quote = single_quote_index != -1
328
+ has_double_quote = double_quote_index != -1
329
+ quote_index = double_quote_index if has_double_quote else single_quote_index
330
+
331
+ has_pipe = (
332
+ has_comma
333
+ and
334
+ has_paren
335
+ and
336
+ (has_single_quote or has_double_quote)
337
+ and not
338
+ (has_double_quote and has_double_quote)
339
+ and not
340
+ (comma_index > paren_index or quote_index > paren_index)
341
+ )
342
+
343
+ if has_pipe:
344
+ code = "_d['pipe'] = Pipe(" + segment[:paren_index + 1]
322
345
  try:
323
346
  exec(code)
324
- _to_add = pipe_repr(_d['pipe']) + segment[paren_index:]
347
+ _to_add = pipe_repr(_d['pipe']) + segment[paren_index + 1:]
325
348
  except Exception as e:
326
349
  _to_add = 'Pipe(' + segment
327
350
  msg += _to_add