meerschaum 2.0.0rc7__py3-none-any.whl → 2.0.0rc9__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 (51) 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/_default.py +1 -1
  20. meerschaum/config/_formatting.py +26 -0
  21. meerschaum/config/_jobs.py +28 -5
  22. meerschaum/config/_paths.py +21 -5
  23. meerschaum/config/_version.py +1 -1
  24. meerschaum/connectors/api/_fetch.py +40 -38
  25. meerschaum/connectors/api/_pipes.py +10 -17
  26. meerschaum/connectors/sql/_fetch.py +29 -11
  27. meerschaum/connectors/sql/_pipes.py +1 -2
  28. meerschaum/core/Pipe/__init__.py +31 -10
  29. meerschaum/core/Pipe/_data.py +23 -13
  30. meerschaum/core/Pipe/_deduplicate.py +44 -23
  31. meerschaum/core/Pipe/_dtypes.py +2 -1
  32. meerschaum/core/Pipe/_fetch.py +29 -0
  33. meerschaum/core/Pipe/_sync.py +25 -18
  34. meerschaum/core/Pipe/_verify.py +60 -25
  35. meerschaum/plugins/__init__.py +3 -0
  36. meerschaum/utils/daemon/Daemon.py +108 -27
  37. meerschaum/utils/daemon/__init__.py +35 -1
  38. meerschaum/utils/dataframe.py +2 -0
  39. meerschaum/utils/formatting/__init__.py +144 -1
  40. meerschaum/utils/formatting/_pipes.py +28 -5
  41. meerschaum/utils/misc.py +184 -188
  42. meerschaum/utils/packages/__init__.py +1 -1
  43. meerschaum/utils/packages/_packages.py +1 -0
  44. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/METADATA +4 -1
  45. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/RECORD +51 -51
  46. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/LICENSE +0 -0
  47. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/NOTICE +0 -0
  48. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/WHEEL +0 -0
  49. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/entry_points.txt +0 -0
  50. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/top_level.txt +0 -0
  51. {meerschaum-2.0.0rc7.dist-info → meerschaum-2.0.0rc9.dist-info}/zip-safe +0 -0
@@ -14,7 +14,16 @@ import threading
14
14
  from datetime import datetime, timedelta
15
15
 
16
16
  from meerschaum.utils.typing import (
17
- Union, Optional, Callable, Any, Tuple, SuccessTuple, Mapping, Dict, List, Iterable, Generator,
17
+ Union,
18
+ Optional,
19
+ Callable,
20
+ Any,
21
+ Tuple,
22
+ SuccessTuple,
23
+ Dict,
24
+ List,
25
+ Iterable,
26
+ Generator,
18
27
  Iterator,
19
28
  )
20
29
 
@@ -29,8 +38,8 @@ def sync(
29
38
  List[Dict[str, Any]],
30
39
  InferFetch
31
40
  ] = InferFetch,
32
- begin: Optional[datetime] = None,
33
- end: Optional[datetime] = None,
41
+ begin: Union[datetime, int, str, None] = '',
42
+ end: Union[datetime, int] = None,
34
43
  force: bool = False,
35
44
  retries: int = 10,
36
45
  min_seconds: int = 1,
@@ -55,28 +64,23 @@ def sync(
55
64
  df: Union[None, pd.DataFrame, Dict[str, List[Any]]], default None
56
65
  An optional DataFrame to sync into the pipe. Defaults to `None`.
57
66
 
58
- begin: Optional[datetime], default None
67
+ begin: Union[datetime, int, str, None], default ''
59
68
  Optionally specify the earliest datetime to search for data.
60
- Defaults to `None`.
61
69
 
62
- end: Optional[datetime], default None
70
+ end: Union[datetime, int, str, None], default None
63
71
  Optionally specify the latest datetime to search for data.
64
- Defaults to `None`.
65
72
 
66
73
  force: bool, default False
67
74
  If `True`, keep trying to sync untul `retries` attempts.
68
- Defaults to `False`.
69
75
 
70
76
  retries: int, default 10
71
77
  If `force`, how many attempts to try syncing before declaring failure.
72
- Defaults to `10`.
73
78
 
74
79
  min_seconds: Union[int, float], default 1
75
80
  If `force`, how many seconds to sleep between retries. Defaults to `1`.
76
81
 
77
82
  check_existing: bool, default True
78
83
  If `True`, pull and diff with existing data from the pipe.
79
- Defaults to `True`.
80
84
 
81
85
  blocking: bool, default True
82
86
  If `True`, wait for sync to finish and return its result, otherwise
@@ -87,7 +91,6 @@ def sync(
87
91
  If provided and the instance connector is thread-safe
88
92
  (`pipe.instance_connector.IS_THREAD_SAFE is True`),
89
93
  limit concurrent sync to this many threads.
90
- Defaults to `None`.
91
94
 
92
95
  callback: Optional[Callable[[Tuple[bool, str]], Any]], default None
93
96
  Callback function which expects a SuccessTuple as input.
@@ -101,7 +104,6 @@ def sync(
101
104
  Specify the number of rows to sync per chunk.
102
105
  If `-1`, resort to system configuration (default is `900`).
103
106
  A `chunksize` of `None` will sync all rows in one transaction.
104
- Defaults to `-1`.
105
107
 
106
108
  sync_chunks: bool, default True
107
109
  If possible, sync chunks while fetching them into memory.
@@ -112,7 +114,6 @@ def sync(
112
114
  Returns
113
115
  -------
114
116
  A `SuccessTuple` of success (`bool`) and message (`str`).
115
-
116
117
  """
117
118
  from meerschaum.utils.debug import dprint, _checkpoint
118
119
  from meerschaum.utils.warnings import warn, error
@@ -183,7 +184,7 @@ def sync(
183
184
  ### use that instead.
184
185
  ### NOTE: The DataFrame must be omitted for the plugin sync method to apply.
185
186
  ### If a DataFrame is provided, continue as expected.
186
- if hasattr(df, 'MRSM_INFER_FETCH'):
187
+ if hasattr(df, 'MRSM_INFER_FETCH'):
187
188
  try:
188
189
  if p.connector is None:
189
190
  msg = f"{p} does not have a valid connector."
@@ -430,13 +431,19 @@ def sync(
430
431
 
431
432
  def _determine_begin(
432
433
  pipe: meerschaum.Pipe,
433
- begin: Optional[datetime] = None,
434
+ begin: Union[datetime, int, str] = '',
434
435
  debug: bool = False,
435
436
  ) -> Union[datetime, int, None]:
436
- ### Datetime has already been provided.
437
- if begin is not None:
437
+ """
438
+ Apply the backtrack interval if `--begin` is not provided.
439
+ """
440
+ if begin != '':
438
441
  return begin
439
- return pipe.get_sync_time(debug=debug)
442
+ sync_time = pipe.get_sync_time(debug=debug)
443
+ if sync_time is None:
444
+ return sync_time
445
+ backtrack_interval = pipe.get_backtrack_interval(debug=debug)
446
+ return sync_time - backtrack_interval
440
447
 
441
448
 
442
449
  def get_sync_time(
@@ -62,6 +62,7 @@ def verify(
62
62
  A SuccessTuple indicating whether the pipe was successfully resynced.
63
63
  """
64
64
  from meerschaum.utils.pool import get_pool
65
+ from meerschaum.utils.misc import interval_str
65
66
  workers = self.get_num_workers(workers)
66
67
 
67
68
  ### Skip configured bounding in parameters
@@ -74,16 +75,16 @@ def verify(
74
75
  if bounded is None:
75
76
  bounded = bound_time is not None
76
77
 
77
- if begin is None:
78
+ if bounded and begin is None:
78
79
  begin = (
79
80
  bound_time
80
81
  if bound_time is not None
81
82
  else self.get_sync_time(newest=False, debug=debug)
82
83
  )
83
- if end is None:
84
+ if bounded and end is None:
84
85
  end = self.get_sync_time(newest=True, debug=debug)
85
86
 
86
- if bounded:
87
+ if bounded and end is not None:
87
88
  end += (
88
89
  timedelta(minutes=1)
89
90
  if isinstance(end, datetime)
@@ -93,13 +94,7 @@ def verify(
93
94
  sync_less_than_begin = not bounded and begin is None
94
95
  sync_greater_than_end = not bounded and end is None
95
96
 
96
- cannot_determine_bounds = (
97
- begin is None
98
- or
99
- end is None
100
- or
101
- not self.exists(debug=debug)
102
- )
97
+ cannot_determine_bounds = not self.exists(debug=debug)
103
98
 
104
99
  if cannot_determine_bounds:
105
100
  sync_success, sync_msg = self.sync(
@@ -146,21 +141,48 @@ def verify(
146
141
  )
147
142
  return True, f"Could not determine chunks between '{begin}' and '{end}'; nothing to do."
148
143
 
144
+ begin_to_print = (
145
+ begin
146
+ if begin is not None
147
+ else (
148
+ chunk_bounds[0][0]
149
+ if bounded
150
+ else chunk_bounds[0][1]
151
+ )
152
+ )
153
+ end_to_print = (
154
+ end
155
+ if end is not None
156
+ else (
157
+ chunk_bounds[-1][1]
158
+ if bounded
159
+ else chunk_bounds[-1][0]
160
+ )
161
+ )
162
+
149
163
  info(
150
164
  f"Syncing {len(chunk_bounds)} chunk" + ('s' if len(chunk_bounds) != 1 else '')
151
165
  + f" ({'un' if not bounded else ''}bounded)"
152
- + f" of size '{chunk_interval}'"
153
- + f" between '{begin}' and '{end}'."
166
+ + f" of size '{interval_str(chunk_interval)}'"
167
+ + f" between '{begin_to_print}' and '{end_to_print}'."
154
168
  )
155
169
 
156
170
  pool = get_pool(workers=workers)
157
171
 
172
+ ### Dictionary of the form bounds -> success_tuple, e.g.:
173
+ ### {
174
+ ### (2023-01-01, 2023-01-02): (True, "Success")
175
+ ### }
176
+ bounds_success_tuples = {}
158
177
  def process_chunk_bounds(
159
178
  chunk_begin_and_end: Tuple[
160
179
  Union[int, datetime],
161
180
  Union[int, datetime]
162
181
  ]
163
182
  ):
183
+ if chunk_begin_and_end in bounds_success_tuples:
184
+ return chunk_begin_and_end, bounds_success_tuples[chunk_begin_and_end]
185
+
164
186
  chunk_begin, chunk_end = chunk_begin_and_end
165
187
  return chunk_begin_and_end, self.sync(
166
188
  begin = chunk_begin,
@@ -171,14 +193,25 @@ def verify(
171
193
  **kwargs
172
194
  )
173
195
 
174
- ### Dictionary of the form bounds -> success_tuple, e.g.:
175
- ### {
176
- ### (2023-01-01, 2023-01-02): (True, "Success")
177
- ### }
178
- bounds_success_tuples = dict(pool.map(process_chunk_bounds, chunk_bounds))
196
+ ### If we have more than one chunk, attempt to sync the first one and return if its fails.
197
+ if len(chunk_bounds) > 1:
198
+ first_chunk_bounds = chunk_bounds[0]
199
+ (
200
+ (first_begin, first_end),
201
+ (first_success, first_msg)
202
+ ) = process_chunk_bounds(first_chunk_bounds)
203
+ if not first_success:
204
+ return (
205
+ first_success,
206
+ f"\n{first_begin} - {first_end}\n"
207
+ + f"Failed to sync first chunk:\n{first_msg}"
208
+ )
209
+ bounds_success_tuples[first_chunk_bounds] = (first_success, first_msg)
210
+
211
+ bounds_success_tuples.update(dict(pool.map(process_chunk_bounds, chunk_bounds)))
179
212
  bounds_success_bools = {bounds: tup[0] for bounds, tup in bounds_success_tuples.items()}
180
213
 
181
- message_header = f"{begin} - {end}"
214
+ message_header = f"{begin_to_print} - {end_to_print}"
182
215
  if all(bounds_success_bools.values()):
183
216
  msg = get_chunks_success_message(bounds_success_tuples, header=message_header)
184
217
  if deduplicate:
@@ -195,18 +228,19 @@ def verify(
195
228
 
196
229
  chunk_bounds_to_resync = [
197
230
  bounds
198
- for bounds, success in zip(chunk_bounds, chunk_success_bools)
231
+ for bounds, success in zip(chunk_bounds, bounds_success_bools)
199
232
  if not success
200
233
  ]
201
234
  bounds_to_print = [
202
235
  f"{bounds[0]} - {bounds[1]}"
203
236
  for bounds in chunk_bounds_to_resync
204
237
  ]
205
- warn(
206
- f"Will resync the following failed chunks:\n "
207
- + '\n '.join(bounds_to_print),
208
- stack = False,
209
- )
238
+ if bounds_to_print:
239
+ warn(
240
+ f"Will resync the following failed chunks:\n "
241
+ + '\n '.join(bounds_to_print),
242
+ stack = False,
243
+ )
210
244
 
211
245
  retry_bounds_success_tuples = dict(pool.map(process_chunk_bounds, chunk_bounds_to_resync))
212
246
  bounds_success_tuples.update(retry_bounds_success_tuples)
@@ -289,7 +323,8 @@ def get_chunks_success_message(
289
323
  ''
290
324
  if num_fails == 0
291
325
  else (
292
- f"\n\nFailed to sync {num_fails} chunks:\n"
326
+ f"\n\nFailed to sync {num_fails} chunk"
327
+ + ('s' if num_fails != 1 else '') + ":\n"
293
328
  + '\n'.join([
294
329
  f"{fail_begin} - {fail_end}\n{msg}\n"
295
330
  for (fail_begin, fail_end), (_, msg) in fail_chunk_bounds_tuples.items()
@@ -254,6 +254,9 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
254
254
  try:
255
255
  if PLUGINS_INTERNAL_LOCK_PATH.exists():
256
256
  PLUGINS_INTERNAL_LOCK_PATH.unlink()
257
+ ### Sometimes competing threads will delete the lock file at the same time.
258
+ except FileNotFoundError:
259
+ pass
257
260
  except Exception as e:
258
261
  if warn:
259
262
  _warn(f"Error cleaning up lockfile {PLUGINS_INTERNAL_LOCK_PATH}:\n{e}")
@@ -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
@@ -651,6 +651,8 @@ def get_datetime_bound_from_df(
651
651
  return best_yet
652
652
 
653
653
  if 'DataFrame' in str(type(df)):
654
+ if datetime_column not in df.columns:
655
+ return None
654
656
  return (
655
657
  df[datetime_column].min(skipna=True)
656
658
  if minimum