meerschaum 2.9.5__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 (200) 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 +198 -118
  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 +382 -95
  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/js/terminado.js +3 -0
  68. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  69. meerschaum/api/resources/templates/termpage.html +13 -0
  70. meerschaum/api/routes/__init__.py +1 -0
  71. meerschaum/api/routes/_actions.py +3 -4
  72. meerschaum/api/routes/_connectors.py +3 -7
  73. meerschaum/api/routes/_jobs.py +26 -35
  74. meerschaum/api/routes/_login.py +120 -15
  75. meerschaum/api/routes/_misc.py +5 -10
  76. meerschaum/api/routes/_pipes.py +178 -143
  77. meerschaum/api/routes/_plugins.py +38 -28
  78. meerschaum/api/routes/_tokens.py +236 -0
  79. meerschaum/api/routes/_users.py +47 -35
  80. meerschaum/api/routes/_version.py +3 -3
  81. meerschaum/api/routes/_webterm.py +3 -3
  82. meerschaum/config/__init__.py +100 -30
  83. meerschaum/config/_default.py +132 -64
  84. meerschaum/config/_edit.py +38 -32
  85. meerschaum/config/_formatting.py +2 -0
  86. meerschaum/config/_patch.py +10 -8
  87. meerschaum/config/_paths.py +133 -13
  88. meerschaum/config/_read_config.py +87 -36
  89. meerschaum/config/_sync.py +6 -3
  90. meerschaum/config/_version.py +1 -1
  91. meerschaum/config/environment.py +262 -0
  92. meerschaum/config/stack/__init__.py +37 -15
  93. meerschaum/config/static.py +18 -0
  94. meerschaum/connectors/_Connector.py +11 -6
  95. meerschaum/connectors/__init__.py +41 -22
  96. meerschaum/connectors/api/_APIConnector.py +34 -6
  97. meerschaum/connectors/api/_actions.py +2 -2
  98. meerschaum/connectors/api/_jobs.py +12 -1
  99. meerschaum/connectors/api/_login.py +33 -7
  100. meerschaum/connectors/api/_misc.py +2 -2
  101. meerschaum/connectors/api/_pipes.py +23 -32
  102. meerschaum/connectors/api/_plugins.py +2 -2
  103. meerschaum/connectors/api/_request.py +1 -1
  104. meerschaum/connectors/api/_tokens.py +146 -0
  105. meerschaum/connectors/api/_users.py +70 -58
  106. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  107. meerschaum/connectors/instance/__init__.py +10 -0
  108. meerschaum/connectors/instance/_pipes.py +442 -0
  109. meerschaum/connectors/instance/_plugins.py +159 -0
  110. meerschaum/connectors/instance/_tokens.py +317 -0
  111. meerschaum/connectors/instance/_users.py +188 -0
  112. meerschaum/connectors/parse.py +5 -2
  113. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  114. meerschaum/connectors/sql/_cli.py +12 -11
  115. meerschaum/connectors/sql/_create_engine.py +12 -168
  116. meerschaum/connectors/sql/_fetch.py +2 -18
  117. meerschaum/connectors/sql/_pipes.py +295 -278
  118. meerschaum/connectors/sql/_plugins.py +29 -0
  119. meerschaum/connectors/sql/_sql.py +46 -21
  120. meerschaum/connectors/sql/_users.py +36 -2
  121. meerschaum/connectors/sql/tables/__init__.py +254 -122
  122. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  123. meerschaum/connectors/valkey/_pipes.py +60 -31
  124. meerschaum/connectors/valkey/_plugins.py +2 -26
  125. meerschaum/core/Pipe/__init__.py +115 -85
  126. meerschaum/core/Pipe/_attributes.py +425 -124
  127. meerschaum/core/Pipe/_bootstrap.py +54 -24
  128. meerschaum/core/Pipe/_cache.py +555 -0
  129. meerschaum/core/Pipe/_clear.py +0 -11
  130. meerschaum/core/Pipe/_data.py +96 -68
  131. meerschaum/core/Pipe/_deduplicate.py +0 -13
  132. meerschaum/core/Pipe/_delete.py +12 -21
  133. meerschaum/core/Pipe/_drop.py +11 -23
  134. meerschaum/core/Pipe/_dtypes.py +49 -19
  135. meerschaum/core/Pipe/_edit.py +14 -4
  136. meerschaum/core/Pipe/_fetch.py +1 -1
  137. meerschaum/core/Pipe/_index.py +8 -14
  138. meerschaum/core/Pipe/_show.py +5 -5
  139. meerschaum/core/Pipe/_sync.py +123 -204
  140. meerschaum/core/Pipe/_verify.py +4 -4
  141. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  142. meerschaum/core/Plugin/__init__.py +1 -1
  143. meerschaum/core/Token/_Token.py +220 -0
  144. meerschaum/core/Token/__init__.py +12 -0
  145. meerschaum/core/User/_User.py +35 -10
  146. meerschaum/core/User/__init__.py +9 -1
  147. meerschaum/core/__init__.py +1 -0
  148. meerschaum/jobs/_Executor.py +88 -4
  149. meerschaum/jobs/_Job.py +149 -38
  150. meerschaum/jobs/__init__.py +3 -2
  151. meerschaum/jobs/systemd.py +8 -3
  152. meerschaum/models/__init__.py +35 -0
  153. meerschaum/models/pipes.py +247 -0
  154. meerschaum/models/tokens.py +38 -0
  155. meerschaum/models/users.py +26 -0
  156. meerschaum/plugins/__init__.py +301 -88
  157. meerschaum/plugins/bootstrap.py +510 -4
  158. meerschaum/utils/_get_pipes.py +97 -30
  159. meerschaum/utils/daemon/Daemon.py +199 -43
  160. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  161. meerschaum/utils/daemon/RotatingFile.py +63 -36
  162. meerschaum/utils/daemon/StdinFile.py +53 -13
  163. meerschaum/utils/daemon/__init__.py +47 -6
  164. meerschaum/utils/daemon/_names.py +6 -3
  165. meerschaum/utils/dataframe.py +479 -81
  166. meerschaum/utils/debug.py +49 -19
  167. meerschaum/utils/dtypes/__init__.py +476 -34
  168. meerschaum/utils/dtypes/sql.py +369 -29
  169. meerschaum/utils/formatting/__init__.py +5 -2
  170. meerschaum/utils/formatting/_jobs.py +1 -1
  171. meerschaum/utils/formatting/_pipes.py +52 -50
  172. meerschaum/utils/formatting/_pprint.py +1 -0
  173. meerschaum/utils/formatting/_shell.py +44 -18
  174. meerschaum/utils/misc.py +268 -186
  175. meerschaum/utils/packages/__init__.py +25 -40
  176. meerschaum/utils/packages/_packages.py +42 -34
  177. meerschaum/utils/pipes.py +213 -0
  178. meerschaum/utils/process.py +2 -2
  179. meerschaum/utils/prompt.py +175 -144
  180. meerschaum/utils/schedule.py +2 -1
  181. meerschaum/utils/sql.py +134 -47
  182. meerschaum/utils/threading.py +42 -0
  183. meerschaum/utils/typing.py +1 -4
  184. meerschaum/utils/venv/_Venv.py +2 -2
  185. meerschaum/utils/venv/__init__.py +7 -7
  186. meerschaum/utils/warnings.py +19 -13
  187. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  188. meerschaum-3.0.0.dist-info/RECORD +289 -0
  189. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  190. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  191. meerschaum/api/models/_interfaces.py +0 -15
  192. meerschaum/api/models/_locations.py +0 -15
  193. meerschaum/api/models/_metrics.py +0 -15
  194. meerschaum/config/_environment.py +0 -145
  195. meerschaum/config/static/__init__.py +0 -186
  196. meerschaum-2.9.5.dist-info/RECORD +0 -263
  197. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  198. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  199. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  200. {meerschaum-2.9.5.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -14,12 +14,13 @@ import traceback
14
14
  import sys
15
15
  import atexit
16
16
  from datetime import datetime, timezone
17
- from typing import List, Optional, Tuple
17
+ from typing import List, Optional, Tuple, Callable
18
18
  from meerschaum.config import get_config
19
19
  from meerschaum.utils.warnings import warn
20
20
  from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInterceptor
21
21
  from meerschaum.utils.threading import Thread
22
22
  import meerschaum as mrsm
23
+ import threading
23
24
  daemon = mrsm.attempt_import('daemon')
24
25
 
25
26
  class RotatingFile(io.IOBase):
@@ -38,6 +39,7 @@ class RotatingFile(io.IOBase):
38
39
  redirect_streams: bool = False,
39
40
  write_timestamps: bool = False,
40
41
  timestamp_format: Optional[str] = None,
42
+ write_callback: Optional[Callable[[str], None]] = None,
41
43
  ):
42
44
  """
43
45
  Create a file-like object which manages other files.
@@ -66,6 +68,9 @@ class RotatingFile(io.IOBase):
66
68
  timestamp_format: str, default None
67
69
  If `write_timestamps` is `True`, use this format for the timestamps.
68
70
  Defaults to `'%Y-%m-%d %H:%M'`.
71
+
72
+ write_callback: Optional[Callable[[str], None]], default None
73
+ If provided, execute this callback with the data to be written.
69
74
  """
70
75
  self.file_path = pathlib.Path(file_path)
71
76
  if num_files_to_keep is None:
@@ -74,17 +79,18 @@ class RotatingFile(io.IOBase):
74
79
  max_file_size = get_config('jobs', 'logs', 'max_file_size')
75
80
  if timestamp_format is None:
76
81
  timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format')
77
- if num_files_to_keep < 2:
78
- raise ValueError("At least 2 files must be kept.")
79
- if max_file_size < 1:
80
- raise ValueError("Subfiles must contain at least one byte.")
82
+ if num_files_to_keep < 1:
83
+ raise ValueError("At least 1 file must be kept.")
84
+ if max_file_size < 100:
85
+ raise ValueError("Subfiles must contain at least 100 bytes.")
81
86
 
82
87
  self.num_files_to_keep = num_files_to_keep
83
88
  self.max_file_size = max_file_size
84
89
  self.redirect_streams = redirect_streams
85
90
  self.write_timestamps = write_timestamps
86
91
  self.timestamp_format = timestamp_format
87
- self.subfile_regex_pattern = re.compile(r'(.*)\.log(?:\.\d+)?$')
92
+ self.write_callback = write_callback
93
+ self.subfile_regex_pattern = re.compile(r'(.*)\.log(?:\.\d+)?')
88
94
 
89
95
  ### When subfiles are opened, map from their index to the file objects.
90
96
  self.subfile_objects = {}
@@ -186,7 +192,7 @@ class RotatingFile(io.IOBase):
186
192
  """
187
193
  try:
188
194
  return int(subfile_name.replace(self.file_path.name + '.', ''))
189
- except Exception as e:
195
+ except Exception:
190
196
  return -1
191
197
 
192
198
 
@@ -272,7 +278,7 @@ class RotatingFile(io.IOBase):
272
278
  try:
273
279
  daemon.daemon.redirect_stream(sys.stdout, self._current_file_obj)
274
280
  daemon.daemon.redirect_stream(sys.stderr, self._current_file_obj)
275
- except OSError as e:
281
+ except OSError:
276
282
  warn(
277
283
  f"Encountered an issue when redirecting streams:\n{traceback.format_exc()}"
278
284
  )
@@ -287,30 +293,36 @@ class RotatingFile(io.IOBase):
287
293
  self.is_subfile_too_large(latest_subfile_index, potential_new_len)
288
294
  )
289
295
  if create_new_file:
290
- old_subfile_index = latest_subfile_index
291
- new_subfile_index = old_subfile_index + 1
292
- new_file_path = self.get_subfile_path_from_index(new_subfile_index)
293
- self._previous_file_obj = self._current_file_obj
294
- self._current_file_obj = open(new_file_path, 'a+', encoding='utf-8')
295
- self.subfile_objects[new_subfile_index] = self._current_file_obj
296
- self.flush()
297
-
298
- if self._previous_file_obj is not None:
299
- if self.redirect_streams:
300
- self._redirected_subfile_objects[old_subfile_index] = self._previous_file_obj
301
- daemon.daemon.redirect_stream(self._previous_file_obj, self._current_file_obj)
302
- daemon.daemon.redirect_stream(sys.stdout, self._current_file_obj)
303
- daemon.daemon.redirect_stream(sys.stderr, self._current_file_obj)
304
- self.close(unused_only=True)
296
+ self.increment_subfiles()
305
297
 
306
- ### Sanity check in case writing somehow fails.
307
- if self._previous_file_obj is self._current_file_obj:
308
- self._previous_file_obj = None
298
+ return self._current_file_obj
309
299
 
310
- self.delete(unused_only=True)
300
+ def increment_subfiles(self, increment_by: int = 1):
301
+ """
302
+ Create a new subfile and switch the file pointer over.
303
+ """
304
+ latest_subfile_index = self.get_latest_subfile_index()
305
+ old_subfile_index = latest_subfile_index
306
+ new_subfile_index = old_subfile_index + increment_by
307
+ new_file_path = self.get_subfile_path_from_index(new_subfile_index)
308
+ self._previous_file_obj = self._current_file_obj
309
+ self._current_file_obj = open(new_file_path, 'a+', encoding='utf-8')
310
+ self.subfile_objects[new_subfile_index] = self._current_file_obj
311
+ self.flush()
311
312
 
312
- return self._current_file_obj
313
+ if self.redirect_streams:
314
+ if self._previous_file_obj is not None:
315
+ self._redirected_subfile_objects[old_subfile_index] = self._previous_file_obj
316
+ daemon.daemon.redirect_stream(self._previous_file_obj, self._current_file_obj)
317
+ daemon.daemon.redirect_stream(sys.stdout, self._current_file_obj)
318
+ daemon.daemon.redirect_stream(sys.stderr, self._current_file_obj)
319
+ self.close(unused_only=True)
320
+
321
+ ### Sanity check in case writing somehow fails.
322
+ if self._previous_file_obj is self._current_file_obj:
323
+ self._previous_file_obj = None
313
324
 
325
+ self.delete(unused_only=True)
314
326
 
315
327
  def close(self, unused_only: bool = False) -> None:
316
328
  """
@@ -330,7 +342,7 @@ class RotatingFile(io.IOBase):
330
342
  try:
331
343
  if not subfile_object.closed:
332
344
  subfile_object.close()
333
- except Exception as e:
345
+ except Exception:
334
346
  warn(f"Failed to close an open subfile:\n{traceback.format_exc()}")
335
347
 
336
348
  _ = self.subfile_objects.pop(subfile_index, None)
@@ -359,6 +371,12 @@ class RotatingFile(io.IOBase):
359
371
  As such, if data is larger than max_file_size, then the corresponding subfile
360
372
  may exceed this limit.
361
373
  """
374
+ try:
375
+ if callable(self.write_callback):
376
+ self.write_callback(data)
377
+ except Exception:
378
+ warn(f"Failed to execute write callback:\n{traceback.format_exc()}")
379
+
362
380
  try:
363
381
  self.file_path.parent.mkdir(exist_ok=True, parents=True)
364
382
  if isinstance(data, bytes):
@@ -379,7 +397,7 @@ class RotatingFile(io.IOBase):
379
397
  except BrokenPipeError:
380
398
  warn("BrokenPipeError encountered. The daemon may have been terminated.")
381
399
  return
382
- except Exception as e:
400
+ except Exception:
383
401
  warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
384
402
  self.flush()
385
403
  self.delete(unused_only=True)
@@ -410,11 +428,10 @@ class RotatingFile(io.IOBase):
410
428
  )
411
429
  for subfile_path_to_delete in existing_subfile_paths[0:end_ix]:
412
430
  subfile_index = self.get_index_from_subfile_name(subfile_path_to_delete.name)
413
- subfile_object = self.subfile_objects.get(subfile_index, None)
414
431
 
415
432
  try:
416
433
  subfile_path_to_delete.unlink()
417
- except Exception as e:
434
+ except Exception:
418
435
  warn(
419
436
  f"Unable to delete subfile '{subfile_path_to_delete}':\n"
420
437
  + f"{traceback.format_exc()}"
@@ -586,20 +603,21 @@ class RotatingFile(io.IOBase):
586
603
  if not subfile_object.closed:
587
604
  try:
588
605
  subfile_object.flush()
589
- except Exception as e:
606
+ except Exception:
590
607
  warn(f"Failed to flush subfile {subfile_index}:\n{traceback.format_exc()}")
608
+
591
609
  if self.redirect_streams:
592
610
  try:
593
611
  sys.stdout.flush()
594
612
  except BrokenPipeError:
595
613
  pass
596
- except Exception as e:
614
+ except Exception:
597
615
  warn(f"Failed to flush STDOUT:\n{traceback.format_exc()}")
598
616
  try:
599
617
  sys.stderr.flush()
600
618
  except BrokenPipeError:
601
619
  pass
602
- except Exception as e:
620
+ except Exception:
603
621
  warn(f"Failed to flush STDERR:\n{traceback.format_exc()}")
604
622
 
605
623
 
@@ -665,10 +683,19 @@ class RotatingFile(io.IOBase):
665
683
  for thread in interceptor_threads[:end_ix]:
666
684
  try:
667
685
  thread.join()
668
- except Exception as e:
686
+ except Exception:
669
687
  warn(f"Failed to join interceptor threads:\n{traceback.format_exc()}")
670
688
  del interceptor_threads[:end_ix]
671
689
 
690
+ def touch(self):
691
+ """
692
+ Touch the latest subfile.
693
+ """
694
+ subfile_path = self.get_latest_subfile_path()
695
+ subfile_path.touch()
696
+
697
+ def isatty(self) -> bool:
698
+ return True
672
699
 
673
700
  def __repr__(self) -> str:
674
701
  """
@@ -13,6 +13,7 @@ import os
13
13
  import selectors
14
14
  import traceback
15
15
 
16
+ import meerschaum as mrsm
16
17
  from meerschaum.utils.typing import Optional, Union
17
18
  from meerschaum.utils.warnings import warn
18
19
 
@@ -25,6 +26,8 @@ class StdinFile(io.TextIOBase):
25
26
  self,
26
27
  file_path: Union[pathlib.Path, str],
27
28
  lock_file_path: Optional[pathlib.Path] = None,
29
+ decode: bool = True,
30
+ refresh_seconds: Union[int, float, None] = None,
28
31
  ):
29
32
  if isinstance(file_path, str):
30
33
  file_path = pathlib.Path(file_path)
@@ -38,6 +41,13 @@ class StdinFile(io.TextIOBase):
38
41
  self._file_handler = None
39
42
  self._fd = None
40
43
  self.sel = selectors.DefaultSelector()
44
+ self.decode = decode
45
+ self._write_fp = None
46
+ self._refresh_seconds = refresh_seconds
47
+
48
+ @property
49
+ def encoding(self):
50
+ return 'utf-8'
41
51
 
42
52
  @property
43
53
  def file_handler(self):
@@ -47,11 +57,9 @@ class StdinFile(io.TextIOBase):
47
57
  if self._file_handler is not None:
48
58
  return self._file_handler
49
59
 
50
- if self.file_path.exists():
51
- self.file_path.unlink()
52
-
53
- self.file_path.parent.mkdir(parents=True, exist_ok=True)
54
- os.mkfifo(self.file_path.as_posix(), mode=0o600)
60
+ if not self.file_path.exists():
61
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
62
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
55
63
 
56
64
  self._fd = os.open(self.file_path, os.O_RDONLY | os.O_NONBLOCK)
57
65
  self._file_handler = os.fdopen(self._fd, 'rb', buffering=0)
@@ -59,11 +67,19 @@ class StdinFile(io.TextIOBase):
59
67
  return self._file_handler
60
68
 
61
69
  def write(self, data):
70
+ if self._write_fp is None:
71
+ self.file_path.parent.mkdir(parents=True, exist_ok=True)
72
+ if not self.file_path.exists():
73
+ os.mkfifo(self.file_path.as_posix(), mode=0o600)
74
+ self._write_fp = open(self.file_path, 'wb')
75
+
62
76
  if isinstance(data, str):
63
77
  data = data.encode('utf-8')
64
-
65
- with open(self.file_path, 'wb') as f:
66
- f.write(data)
78
+ try:
79
+ self._write_fp.write(data)
80
+ self._write_fp.flush()
81
+ except BrokenPipeError:
82
+ pass
67
83
 
68
84
  def fileno(self):
69
85
  fileno = self.file_handler.fileno()
@@ -83,18 +99,19 @@ class StdinFile(io.TextIOBase):
83
99
  self.blocking_file_path.unlink()
84
100
  except Exception:
85
101
  warn(traceback.format_exc())
86
- return data.decode('utf-8')
102
+ return data.decode('utf-8') if self.decode else data
87
103
  except (OSError, EOFError):
88
104
  pass
89
105
 
90
- self.blocking_file_path.touch()
91
- time.sleep(0.1)
106
+ if not self.blocking_file_path.exists():
107
+ self.blocking_file_path.touch()
108
+ time.sleep(self.refresh_seconds)
92
109
 
93
110
  def readline(self, size=-1):
94
- line = ''
111
+ line = '' if self.decode else b''
95
112
  while True:
96
113
  data = self.read(1)
97
- if not data or data == '\n':
114
+ if not data or ((data == '\n') if self.decode else (data == b'\n')):
98
115
  break
99
116
  line += data
100
117
 
@@ -111,11 +128,34 @@ class StdinFile(io.TextIOBase):
111
128
  self._file_handler = None
112
129
  self._fd = None
113
130
 
131
+ if self._write_fp is not None:
132
+ try:
133
+ self._write_fp.close()
134
+ except BrokenPipeError:
135
+ pass
136
+ self._write_fp = None
137
+
138
+ try:
139
+ if self.blocking_file_path.exists():
140
+ self.blocking_file_path.unlink()
141
+ except Exception:
142
+ pass
114
143
  super().close()
115
144
 
116
145
  def is_open(self):
117
146
  return self._file_handler is not None
118
147
 
148
+ def isatty(self) -> bool:
149
+ return False
150
+
151
+ @property
152
+ def refresh_seconds(self) -> Union[int, float]:
153
+ """
154
+ How many seconds between checking for blocking functions.
155
+ """
156
+ if not self._refresh_seconds:
157
+ self._refresh_seconds = mrsm.get_config('system', 'cli', 'refresh_seconds')
158
+ return self._refresh_seconds
119
159
 
120
160
  def __str__(self) -> str:
121
161
  return f"StdinFile('{self.file_path}')"
@@ -7,9 +7,16 @@ Manage Daemons via the `Daemon` class.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- import os, pathlib, shutil, json, datetime, threading, shlex
11
- from meerschaum.utils.typing import SuccessTuple, List, Optional, Callable, Any, Dict
12
- from meerschaum.config._paths import DAEMON_RESOURCES_PATH
10
+
11
+ import os
12
+ import pathlib
13
+ import shutil
14
+ import json
15
+ import datetime
16
+ import threading
17
+ import shlex
18
+
19
+ from meerschaum.utils.typing import SuccessTuple, List, Optional, Callable, Any, Dict, Union
13
20
  from meerschaum.utils.daemon.StdinFile import StdinFile
14
21
  from meerschaum.utils.daemon.Daemon import Daemon
15
22
  from meerschaum.utils.daemon.RotatingFile import RotatingFile
@@ -17,6 +24,27 @@ from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInte
17
24
  from meerschaum.utils.daemon._names import get_new_daemon_name
18
25
 
19
26
 
27
+ __all__ = (
28
+ 'daemon_action',
29
+ 'daemon_entry',
30
+ 'get_daemons',
31
+ 'get_daemon_ids',
32
+ 'get_running_daemons',
33
+ 'get_stopped_daemons',
34
+ 'get_paused_daemons',
35
+ 'get_filtered_daemons',
36
+ 'get_new_daemon_name',
37
+ 'run_daemon',
38
+ 'running_in_daemon',
39
+ 'Daemon',
40
+ 'StdinFile',
41
+ 'RotatingFile',
42
+ 'FileDescriptorInterceptor',
43
+ )
44
+
45
+ _daemons = {}
46
+
47
+
20
48
  def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
21
49
  """Parse sysargs and execute a Meerschaum action as a daemon.
22
50
 
@@ -183,6 +211,7 @@ def get_daemon_ids() -> List[str]:
183
211
  """
184
212
  Return the IDs of all daemons on disk.
185
213
  """
214
+ from meerschaum.config._paths import DAEMON_RESOURCES_PATH
186
215
  return [
187
216
  daemon_dir
188
217
  for daemon_dir in sorted(os.listdir(DAEMON_RESOURCES_PATH))
@@ -270,8 +299,6 @@ def get_filtered_daemons(
270
299
  if warn:
271
300
  _warn(f"Daemon '{d_id}' does not exist.", stack=False)
272
301
  continue
273
- if d.hidden:
274
- pass
275
302
  daemons.append(d)
276
303
  return daemons
277
304
 
@@ -280,6 +307,20 @@ def running_in_daemon() -> bool:
280
307
  """
281
308
  Return whether the current thread is running in a Daemon context.
282
309
  """
283
- from meerschaum.config.static import STATIC_CONFIG
310
+ from meerschaum._internal.static import STATIC_CONFIG
284
311
  daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
285
312
  return daemon_env_var in os.environ
313
+
314
+
315
+ def get_current_daemon() -> Union[Daemon, None]:
316
+ """
317
+ If running withing a daemon context, return the corresponding `Daemon`.
318
+ Otherwise return `None`.
319
+ """
320
+ from meerschaum._internal.static import STATIC_CONFIG
321
+ daemon_env_var = STATIC_CONFIG['environment']['daemon_id']
322
+ daemon_id = os.environ.get(daemon_env_var, None)
323
+ if daemon_id is None:
324
+ return None
325
+
326
+ return _daemons.get(daemon_id, Daemon(daemon_id=daemon_id))
@@ -7,9 +7,11 @@ Generate random names for jobs.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- import os, random
11
- from meerschaum.utils.typing import Dict, List, Tuple
12
- from meerschaum.config._paths import DAEMON_RESOURCES_PATH
10
+
11
+ import os
12
+ import random
13
+
14
+ from meerschaum.utils.typing import Dict, List
13
15
 
14
16
  _bank: Dict[str, Dict[str, List[str]]] = {
15
17
  'adjectives': {
@@ -116,6 +118,7 @@ def get_new_daemon_name() -> str:
116
118
  Generate a new random name until a unique one is found
117
119
  (up to ~6000 maximum possibilities).
118
120
  """
121
+ from meerschaum.config._paths import DAEMON_RESOURCES_PATH
119
122
  existing_names = (
120
123
  os.listdir(DAEMON_RESOURCES_PATH)
121
124
  if DAEMON_RESOURCES_PATH.exists()