meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc7__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 (117) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +434 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +113 -19
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +3 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +2 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +29 -0
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/register.py +9 -2
  34. meerschaum/api/dash/pages/login.py +2 -2
  35. meerschaum/api/dash/pipes.py +72 -36
  36. meerschaum/api/dash/webterm.py +14 -6
  37. meerschaum/api/models/_pipes.py +7 -1
  38. meerschaum/api/resources/static/js/terminado.js +3 -0
  39. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  40. meerschaum/api/resources/templates/termpage.html +1 -0
  41. meerschaum/api/routes/_jobs.py +23 -11
  42. meerschaum/api/routes/_login.py +73 -5
  43. meerschaum/api/routes/_pipes.py +6 -4
  44. meerschaum/api/routes/_webterm.py +3 -3
  45. meerschaum/config/__init__.py +60 -13
  46. meerschaum/config/_default.py +89 -61
  47. meerschaum/config/_edit.py +10 -8
  48. meerschaum/config/_formatting.py +2 -0
  49. meerschaum/config/_patch.py +4 -2
  50. meerschaum/config/_paths.py +127 -12
  51. meerschaum/config/_read_config.py +20 -10
  52. meerschaum/config/_version.py +1 -1
  53. meerschaum/config/environment.py +262 -0
  54. meerschaum/config/stack/__init__.py +7 -5
  55. meerschaum/connectors/_Connector.py +1 -2
  56. meerschaum/connectors/__init__.py +37 -2
  57. meerschaum/connectors/api/_APIConnector.py +1 -1
  58. meerschaum/connectors/api/_jobs.py +11 -0
  59. meerschaum/connectors/api/_pipes.py +7 -1
  60. meerschaum/connectors/instance/_plugins.py +9 -1
  61. meerschaum/connectors/instance/_tokens.py +20 -3
  62. meerschaum/connectors/instance/_users.py +8 -1
  63. meerschaum/connectors/parse.py +1 -1
  64. meerschaum/connectors/sql/_create_engine.py +3 -0
  65. meerschaum/connectors/sql/_pipes.py +93 -79
  66. meerschaum/connectors/sql/_users.py +8 -1
  67. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  68. meerschaum/connectors/valkey/_pipes.py +7 -5
  69. meerschaum/core/Pipe/__init__.py +45 -71
  70. meerschaum/core/Pipe/_attributes.py +66 -90
  71. meerschaum/core/Pipe/_cache.py +555 -0
  72. meerschaum/core/Pipe/_clear.py +0 -11
  73. meerschaum/core/Pipe/_data.py +0 -50
  74. meerschaum/core/Pipe/_deduplicate.py +0 -13
  75. meerschaum/core/Pipe/_delete.py +12 -21
  76. meerschaum/core/Pipe/_drop.py +11 -23
  77. meerschaum/core/Pipe/_dtypes.py +1 -1
  78. meerschaum/core/Pipe/_index.py +8 -14
  79. meerschaum/core/Pipe/_sync.py +12 -18
  80. meerschaum/core/Plugin/_Plugin.py +7 -1
  81. meerschaum/core/Token/_Token.py +1 -1
  82. meerschaum/core/User/_User.py +1 -2
  83. meerschaum/jobs/_Executor.py +88 -4
  84. meerschaum/jobs/_Job.py +135 -35
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +195 -41
  88. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  89. meerschaum/utils/daemon/RotatingFile.py +63 -36
  90. meerschaum/utils/daemon/StdinFile.py +53 -13
  91. meerschaum/utils/daemon/__init__.py +18 -5
  92. meerschaum/utils/daemon/_names.py +6 -3
  93. meerschaum/utils/debug.py +34 -4
  94. meerschaum/utils/dtypes/__init__.py +5 -1
  95. meerschaum/utils/formatting/__init__.py +4 -1
  96. meerschaum/utils/formatting/_jobs.py +1 -1
  97. meerschaum/utils/formatting/_pipes.py +47 -46
  98. meerschaum/utils/formatting/_shell.py +16 -6
  99. meerschaum/utils/misc.py +18 -38
  100. meerschaum/utils/packages/__init__.py +15 -13
  101. meerschaum/utils/packages/_packages.py +1 -0
  102. meerschaum/utils/pipes.py +33 -5
  103. meerschaum/utils/process.py +1 -1
  104. meerschaum/utils/prompt.py +171 -144
  105. meerschaum/utils/sql.py +12 -2
  106. meerschaum/utils/threading.py +42 -0
  107. meerschaum/utils/venv/__init__.py +2 -0
  108. meerschaum/utils/warnings.py +19 -13
  109. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/zip-safe +0 -0
meerschaum/jobs/_Job.py CHANGED
@@ -12,8 +12,8 @@ import shlex
12
12
  import asyncio
13
13
  import pathlib
14
14
  import sys
15
+ import json
15
16
  import traceback
16
- from functools import partial
17
17
  from datetime import datetime, timezone
18
18
 
19
19
  import meerschaum as mrsm
@@ -25,6 +25,7 @@ from meerschaum.utils.warnings import warn
25
25
  from meerschaum.config.paths import LOGS_RESOURCES_PATH
26
26
  from meerschaum.config import get_config
27
27
  from meerschaum._internal.static import STATIC_CONFIG
28
+ from meerschaum.utils.formatting._shell import clear_screen
28
29
 
29
30
  if TYPE_CHECKING:
30
31
  from meerschaum.jobs._Executor import Executor
@@ -39,6 +40,8 @@ RESTART_FLAGS: List[str] = [
39
40
  '--schedule',
40
41
  '--cron',
41
42
  ]
43
+ STOP_TOKEN: str = STATIC_CONFIG['jobs']['stop_token']
44
+ CLEAR_TOKEN: str = STATIC_CONFIG['jobs']['clear_token']
42
45
 
43
46
  class StopMonitoringLogs(Exception):
44
47
  """
@@ -46,6 +49,20 @@ class StopMonitoringLogs(Exception):
46
49
  """
47
50
 
48
51
 
52
+ def _default_stdout_callback(line: str):
53
+ if line == '\n':
54
+ print('', end='', flush=True)
55
+ return
56
+
57
+ if CLEAR_TOKEN in line:
58
+ clear_screen()
59
+ return
60
+
61
+ if STOP_TOKEN in line:
62
+ return
63
+
64
+ print(line, end='', flush=True)
65
+
49
66
  class Job:
50
67
  """
51
68
  Manage a `meerschaum.utils.daemon.Daemon`, locally or remotely via the API.
@@ -58,6 +75,7 @@ class Job:
58
75
  env: Optional[Dict[str, str]] = None,
59
76
  executor_keys: Optional[str] = None,
60
77
  delete_after_completion: bool = False,
78
+ refresh_seconds: Union[int, float, None] = None,
61
79
  _properties: Optional[Dict[str, Any]] = None,
62
80
  _rotating_log=None,
63
81
  _stdin_file=None,
@@ -86,6 +104,10 @@ class Job:
86
104
  delete_after_completion: bool, default False
87
105
  If `True`, delete this job when it has finished executing.
88
106
 
107
+ refresh_seconds: Union[int, float, None], default None
108
+ The number of seconds to sleep between refreshes.
109
+ Defaults to the configured value `system.cli.refresh_seconds`.
110
+
89
111
  _properties: Optional[Dict[str, Any]], default None
90
112
  If provided, use this to patch the daemon's properties.
91
113
  """
@@ -112,6 +134,11 @@ class Job:
112
134
 
113
135
  self.executor_keys = executor_keys
114
136
  self.name = name
137
+ self.refresh_seconds = (
138
+ refresh_seconds
139
+ if refresh_seconds is not None
140
+ else mrsm.get_config('system', 'cli', 'refresh_seconds')
141
+ )
115
142
  try:
116
143
  self._daemon = (
117
144
  Daemon(daemon_id=name)
@@ -257,7 +284,11 @@ class Job:
257
284
 
258
285
  return success, f"Started {self}."
259
286
 
260
- def stop(self, timeout_seconds: Optional[int] = None, debug: bool = False) -> SuccessTuple:
287
+ def stop(
288
+ self,
289
+ timeout_seconds: Union[int, float, None] = None,
290
+ debug: bool = False,
291
+ ) -> SuccessTuple:
261
292
  """
262
293
  Stop the job's daemon.
263
294
  """
@@ -284,7 +315,11 @@ class Job:
284
315
 
285
316
  return kill_success, f"Killed {self}."
286
317
 
287
- def pause(self, timeout_seconds: Optional[int] = None, debug: bool = False) -> SuccessTuple:
318
+ def pause(
319
+ self,
320
+ timeout_seconds: Union[int, float, None] = None,
321
+ debug: bool = False,
322
+ ) -> SuccessTuple:
288
323
  """
289
324
  Pause the job's daemon.
290
325
  """
@@ -342,7 +377,7 @@ class Job:
342
377
 
343
378
  def monitor_logs(
344
379
  self,
345
- callback_function: Callable[[str], None] = partial(print, end=''),
380
+ callback_function: Callable[[str], None] = _default_stdout_callback,
346
381
  input_callback_function: Optional[Callable[[], str]] = None,
347
382
  stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
348
383
  stop_event: Optional[asyncio.Event] = None,
@@ -350,6 +385,10 @@ class Job:
350
385
  strip_timestamps: bool = False,
351
386
  accept_input: bool = True,
352
387
  debug: bool = False,
388
+ _logs_path: Optional[pathlib.Path] = None,
389
+ _log=None,
390
+ _stdin_file=None,
391
+ _wait_if_stopped: bool = True,
353
392
  ):
354
393
  """
355
394
  Monitor the job's log files and execute a callback on new lines.
@@ -382,12 +421,6 @@ class Job:
382
421
  accept_input: bool, default True
383
422
  If `True`, accept input when the daemon blocks on stdin.
384
423
  """
385
- def default_input_callback_function():
386
- return sys.stdin.readline()
387
-
388
- if input_callback_function is None:
389
- input_callback_function = default_input_callback_function
390
-
391
424
  if self.executor is not None:
392
425
  self.executor.monitor_logs(
393
426
  self.name,
@@ -409,29 +442,35 @@ class Job:
409
442
  stop_on_exit=stop_on_exit,
410
443
  strip_timestamps=strip_timestamps,
411
444
  accept_input=accept_input,
445
+ debug=debug,
446
+ _logs_path=_logs_path,
447
+ _log=_log,
448
+ _stdin_file=_stdin_file,
449
+ _wait_if_stopped=_wait_if_stopped,
412
450
  )
413
451
  return asyncio.run(monitor_logs_coroutine)
414
452
 
415
453
  async def monitor_logs_async(
416
454
  self,
417
- callback_function: Callable[[str], None] = partial(print, end='', flush=True),
455
+ callback_function: Callable[[str], None] = _default_stdout_callback,
418
456
  input_callback_function: Optional[Callable[[], str]] = None,
419
457
  stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None,
420
458
  stop_event: Optional[asyncio.Event] = None,
421
459
  stop_on_exit: bool = False,
422
460
  strip_timestamps: bool = False,
423
461
  accept_input: bool = True,
462
+ debug: bool = False,
424
463
  _logs_path: Optional[pathlib.Path] = None,
425
464
  _log=None,
426
465
  _stdin_file=None,
427
- debug: bool = False,
466
+ _wait_if_stopped: bool = True,
428
467
  ):
429
468
  """
430
469
  Monitor the job's log files and await a callback on new lines.
431
470
 
432
471
  Parameters
433
472
  ----------
434
- callback_function: Callable[[str], None], default partial(print, end='')
473
+ callback_function: Callable[[str], None], default _default_stdout_callback
435
474
  The callback to execute as new data comes in.
436
475
  Defaults to printing the output directly to `stdout`.
437
476
 
@@ -457,7 +496,13 @@ class Job:
457
496
  accept_input: bool, default True
458
497
  If `True`, accept input when the daemon blocks on stdin.
459
498
  """
499
+ from meerschaum.utils.prompt import prompt
500
+
460
501
  def default_input_callback_function():
502
+ prompt_kwargs = self.get_prompt_kwargs(debug=debug)
503
+ if prompt_kwargs:
504
+ answer = prompt(**prompt_kwargs)
505
+ return answer + '\n'
461
506
  return sys.stdin.readline()
462
507
 
463
508
  if input_callback_function is None:
@@ -481,23 +526,26 @@ class Job:
481
526
  events = {
482
527
  'user': stop_event,
483
528
  'stopped': asyncio.Event(),
529
+ 'stop_token': asyncio.Event(),
530
+ 'stop_exception': asyncio.Event(),
531
+ 'stopped_timeout': asyncio.Event(),
484
532
  }
485
533
  combined_event = asyncio.Event()
486
534
  emitted_text = False
487
535
  stdin_file = _stdin_file if _stdin_file is not None else self.daemon.stdin_file
488
536
 
489
537
  async def check_job_status():
490
- nonlocal emitted_text
491
- stopped_event = events.get('stopped', None)
492
- if stopped_event is None:
538
+ if not stop_on_exit:
493
539
  return
494
540
 
541
+ nonlocal emitted_text
542
+
495
543
  sleep_time = 0.1
496
- while sleep_time < 60:
544
+ while sleep_time < 0.2:
497
545
  if self.status == 'stopped':
498
- if not emitted_text:
546
+ if not emitted_text and _wait_if_stopped:
499
547
  await asyncio.sleep(sleep_time)
500
- sleep_time = round(sleep_time * 1.1, 2)
548
+ sleep_time = round(sleep_time * 1.1, 3)
501
549
  continue
502
550
 
503
551
  if stop_callback_function is not None:
@@ -517,11 +565,13 @@ class Job:
517
565
  break
518
566
  await asyncio.sleep(0.1)
519
567
 
568
+ events['stopped_timeout'].set()
569
+
520
570
  async def check_blocking_on_input():
521
571
  while True:
522
572
  if not emitted_text or not self.is_blocking_on_stdin():
523
573
  try:
524
- await asyncio.sleep(0.1)
574
+ await asyncio.sleep(self.refresh_seconds)
525
575
  except asyncio.exceptions.CancelledError:
526
576
  break
527
577
  continue
@@ -536,14 +586,15 @@ class Job:
536
586
  if asyncio.iscoroutinefunction(input_callback_function):
537
587
  data = await input_callback_function()
538
588
  else:
539
- data = input_callback_function()
589
+ loop = asyncio.get_running_loop()
590
+ data = await loop.run_in_executor(None, input_callback_function)
540
591
  except KeyboardInterrupt:
541
592
  break
542
- if not data.endswith('\n'):
543
- data += '\n'
593
+ # if not data.endswith('\n'):
594
+ # data += '\n'
544
595
 
545
596
  stdin_file.write(data)
546
- await asyncio.sleep(0.1)
597
+ await asyncio.sleep(self.refresh_seconds)
547
598
 
548
599
  async def combine_events():
549
600
  event_tasks = [
@@ -571,15 +622,30 @@ class Job:
571
622
  combine_events_task = asyncio.create_task(combine_events())
572
623
 
573
624
  log = _log if _log is not None else self.daemon.rotating_log
574
- lines_to_show = get_config('jobs', 'logs', 'lines_to_show')
625
+ lines_to_show = (
626
+ self.daemon.properties.get(
627
+ 'logs', {}
628
+ ).get(
629
+ 'lines_to_show', get_config('jobs', 'logs', 'lines_to_show')
630
+ )
631
+ )
575
632
 
576
633
  async def emit_latest_lines():
577
634
  nonlocal emitted_text
635
+ nonlocal stop_event
578
636
  lines = log.readlines()
579
637
  for line in lines[(-1 * lines_to_show):]:
580
638
  if stop_event is not None and stop_event.is_set():
581
639
  return
582
640
 
641
+ if strip_timestamp_from_line(line.strip()) == STOP_TOKEN:
642
+ events['stop_token'].set()
643
+ return
644
+
645
+ if strip_timestamp_from_line(line.strip()) == CLEAR_TOKEN:
646
+ clear_screen(debug=debug)
647
+ continue
648
+
583
649
  if strip_timestamps:
584
650
  line = strip_timestamp_from_line(line)
585
651
 
@@ -590,6 +656,7 @@ class Job:
590
656
  callback_function(line)
591
657
  emitted_text = True
592
658
  except StopMonitoringLogs:
659
+ events['stop_exception'].set()
593
660
  return
594
661
  except Exception:
595
662
  warn(f"Error in logs callback:\n{traceback.format_exc()}")
@@ -608,9 +675,14 @@ class Job:
608
675
  except Exception:
609
676
  warn(f"Failed to run async checks:\n{traceback.format_exc()}")
610
677
 
611
- watchfiles = mrsm.attempt_import('watchfiles')
678
+ watchfiles = mrsm.attempt_import('watchfiles', lazy=False)
679
+ dir_path_to_monitor = (
680
+ _logs_path
681
+ or (log.file_path.parent if log else None)
682
+ or LOGS_RESOURCES_PATH
683
+ )
612
684
  async for changes in watchfiles.awatch(
613
- _logs_path or LOGS_RESOURCES_PATH,
685
+ dir_path_to_monitor,
614
686
  stop_event=combined_event,
615
687
  ):
616
688
  for change in changes:
@@ -633,6 +705,27 @@ class Job:
633
705
 
634
706
  return self.is_running() and self.daemon.blocking_stdin_file_path.exists()
635
707
 
708
+ def get_prompt_kwargs(self, debug: bool = False) -> Dict[str, Any]:
709
+ """
710
+ Return the kwargs to the blocking `prompt()`, if available.
711
+ """
712
+ if self.executor is not None:
713
+ return self.executor.get_job_prompt_kwargs(self.name, debug=debug)
714
+
715
+ if not self.daemon.prompt_kwargs_file_path.exists():
716
+ return {}
717
+
718
+ try:
719
+ with open(self.daemon.prompt_kwargs_file_path, 'r', encoding='utf-8') as f:
720
+ prompt_kwargs = json.load(f)
721
+
722
+ return prompt_kwargs
723
+
724
+ except Exception:
725
+ import traceback
726
+ traceback.print_exc()
727
+ return {}
728
+
636
729
  def write_stdin(self, data):
637
730
  """
638
731
  Write to a job's daemon's `stdin`.
@@ -724,6 +817,20 @@ class Job:
724
817
  self._sysargs = target_args[0] if len(target_args) > 0 else []
725
818
  return self._sysargs
726
819
 
820
+ def get_daemon_properties(self) -> Dict[str, Any]:
821
+ """
822
+ Return the `properties` dictionary for the job's daemon.
823
+ """
824
+ remote_properties = (
825
+ {}
826
+ if self.executor is None
827
+ else self.executor.get_job_properties(self.name)
828
+ )
829
+ return {
830
+ **remote_properties,
831
+ **self._properties_patch
832
+ }
833
+
727
834
  @property
728
835
  def daemon(self) -> 'Daemon':
729
836
  """
@@ -733,20 +840,13 @@ class Job:
733
840
  if self._daemon is not None and self.executor is None and self._sysargs:
734
841
  return self._daemon
735
842
 
736
- remote_properties = (
737
- {}
738
- if self.executor is None
739
- else self.executor.get_job_properties(self.name)
740
- )
741
- properties = {**remote_properties, **self._properties_patch}
742
-
743
843
  self._daemon = Daemon(
744
844
  target=entry,
745
845
  target_args=[self._sysargs],
746
846
  target_kw={},
747
847
  daemon_id=self.name,
748
848
  label=shlex.join(self._sysargs),
749
- properties=properties,
849
+ properties=self.get_daemon_properties(),
750
850
  )
751
851
  if '_rotating_log' in self.__dict__:
752
852
  self._daemon._rotating_log = self._rotating_log
@@ -12,7 +12,6 @@ import sys
12
12
  import asyncio
13
13
  import json
14
14
  import time
15
- import traceback
16
15
  import shutil
17
16
  from datetime import datetime, timezone
18
17
  from functools import partial
@@ -23,7 +22,6 @@ from meerschaum.utils.typing import Dict, Any, List, SuccessTuple, Union, Option
23
22
  from meerschaum.config import get_config
24
23
  from meerschaum._internal.static import STATIC_CONFIG
25
24
  from meerschaum.utils.warnings import warn, dprint
26
- from meerschaum._internal.arguments._parse_arguments import parse_arguments
27
25
 
28
26
  JOB_METADATA_CACHE_SECONDS: int = STATIC_CONFIG['api']['jobs']['metadata_cache_seconds']
29
27
 
@@ -705,6 +703,13 @@ class SystemdExecutor(Executor):
705
703
  blocking_path = socket_path.parent / (socket_path.name + '.block')
706
704
  return blocking_path.exists()
707
705
 
706
+ def get_job_prompt_kwargs(self, name: str, debug: bool = False) -> Dict[str, Any]:
707
+ """
708
+ Return the kwargs to the blocking prompt.
709
+ """
710
+ job = self.get_hidden_job(name, debug=debug)
711
+ return job.get_prompt_kwargs(debug=debug)
712
+
708
713
  def get_job_rotating_file(self, name: str, debug: bool = False):
709
714
  """
710
715
  Return a `RotatingFile` for the job's log output.