meerschaum 2.2.0.dev3__py3-none-any.whl → 2.2.0rc1__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.
- meerschaum/_internal/entry.py +1 -1
- meerschaum/actions/show.py +67 -6
- meerschaum/config/_version.py +1 -1
- meerschaum/utils/daemon/Daemon.py +50 -32
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +60 -0
- meerschaum/utils/daemon/RotatingFile.py +89 -2
- meerschaum/utils/packages/__init__.py +9 -2
- meerschaum/utils/packages/_packages.py +3 -3
- meerschaum/utils/schedule.py +41 -47
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/METADATA +10 -9
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/RECORD +17 -16
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc1.dist-info}/zip-safe +0 -0
meerschaum/_internal/entry.py
CHANGED
@@ -49,7 +49,7 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
|
|
49
49
|
|
50
50
|
if args.get('schedule', None):
|
51
51
|
from meerschaum.utils.schedule import schedule_function
|
52
|
-
return schedule_function(entry_with_args,
|
52
|
+
return schedule_function(entry_with_args, **args)
|
53
53
|
return entry_with_args(**args)
|
54
54
|
|
55
55
|
|
meerschaum/actions/show.py
CHANGED
@@ -41,6 +41,7 @@ def show(
|
|
41
41
|
'jobs' : _show_jobs,
|
42
42
|
'logs' : _show_logs,
|
43
43
|
'tags' : _show_tags,
|
44
|
+
'schedules' : _show_schedules,
|
44
45
|
}
|
45
46
|
return choose_subaction(action, show_options, **kw)
|
46
47
|
|
@@ -577,6 +578,7 @@ def _show_logs(
|
|
577
578
|
`show logs myjob myotherjob`
|
578
579
|
"""
|
579
580
|
import os, pathlib, random, asyncio
|
581
|
+
from datetime import datetime
|
580
582
|
from meerschaum.utils.packages import attempt_import, import_rich
|
581
583
|
from meerschaum.utils.daemon import get_filtered_daemons, Daemon
|
582
584
|
from meerschaum.utils.warnings import warn, info
|
@@ -622,13 +624,22 @@ def _show_logs(
|
|
622
624
|
return _job_colors[daemon_id]
|
623
625
|
|
624
626
|
def _follow_pretty_print():
|
625
|
-
|
627
|
+
watchfiles = attempt_import('watchfiles')
|
626
628
|
rich = import_rich()
|
627
629
|
rich_text = attempt_import('rich.text')
|
628
630
|
_watch_daemon_ids = {d.daemon_id: d for d in daemons}
|
629
631
|
info("Watching log files...")
|
630
632
|
|
631
633
|
def _print_job_line(daemon, line):
|
634
|
+
date_prefix_str = line[:len('YYYY-MM-DD HH:mm')]
|
635
|
+
try:
|
636
|
+
line_timestamp = datetime.fromisoformat(date_prefix_str)
|
637
|
+
except Exception as e:
|
638
|
+
line_timestamp = None
|
639
|
+
if line_timestamp:
|
640
|
+
line = line[len('YYYY-MM-DD HH:mm | '):]
|
641
|
+
if len(line) == 0:
|
642
|
+
return
|
632
643
|
text = rich_text.Text(daemon.daemon_id)
|
633
644
|
text.append(
|
634
645
|
_get_buffer_spaces(daemon.daemon_id) + '| '
|
@@ -676,8 +687,8 @@ def _show_logs(
|
|
676
687
|
_print_log_lines(d)
|
677
688
|
|
678
689
|
_quit = False
|
679
|
-
|
680
|
-
|
690
|
+
def _watch_logs():
|
691
|
+
for changes in watchfiles.watch(LOGS_RESOURCES_PATH):
|
681
692
|
if _quit:
|
682
693
|
return
|
683
694
|
for change in changes:
|
@@ -699,10 +710,9 @@ def _show_logs(
|
|
699
710
|
if daemon is not None:
|
700
711
|
_print_log_lines(daemon)
|
701
712
|
|
702
|
-
loop = asyncio.new_event_loop()
|
703
713
|
try:
|
704
|
-
|
705
|
-
except KeyboardInterrupt:
|
714
|
+
_watch_logs()
|
715
|
+
except KeyboardInterrupt as ki:
|
706
716
|
_quit = True
|
707
717
|
|
708
718
|
def _print_nopretty_log_text():
|
@@ -817,6 +827,57 @@ def _show_tags(
|
|
817
827
|
return True, "Success"
|
818
828
|
|
819
829
|
|
830
|
+
def _show_schedules(
|
831
|
+
action: Optional[List[str]] = None,
|
832
|
+
nopretty: bool = False,
|
833
|
+
**kwargs: Any
|
834
|
+
) -> SuccessTuple:
|
835
|
+
"""
|
836
|
+
Print the upcoming timestamps according to the given schedule.
|
837
|
+
|
838
|
+
Examples:
|
839
|
+
show schedule 'daily starting 00:00'
|
840
|
+
show schedule 'every 12 hours and mon-fri starting 2024-01-01'
|
841
|
+
"""
|
842
|
+
from meerschaum.utils.schedule import parse_schedule
|
843
|
+
from meerschaum.utils.misc import is_int
|
844
|
+
from meerschaum.utils.formatting import print_options
|
845
|
+
if not action:
|
846
|
+
return False, "Provide a schedule to be parsed."
|
847
|
+
schedule = action[0]
|
848
|
+
default_num_timestamps = 5
|
849
|
+
num_timestamps_str = action[1] if len(action) >= 2 else str(default_num_timestamps)
|
850
|
+
num_timestamps = (
|
851
|
+
int(num_timestamps_str)
|
852
|
+
if is_int(num_timestamps_str)
|
853
|
+
else default_num_timestamps
|
854
|
+
)
|
855
|
+
try:
|
856
|
+
trigger = parse_schedule(schedule)
|
857
|
+
except ValueError as e:
|
858
|
+
return False, str(e)
|
859
|
+
|
860
|
+
next_datetimes = []
|
861
|
+
for _ in range(num_timestamps):
|
862
|
+
try:
|
863
|
+
next_dt = trigger.next()
|
864
|
+
next_datetimes.append(next_dt)
|
865
|
+
except Exception as e:
|
866
|
+
break
|
867
|
+
|
868
|
+
print_options(
|
869
|
+
next_datetimes,
|
870
|
+
num_cols = 1,
|
871
|
+
nopretty = nopretty,
|
872
|
+
header = (
|
873
|
+
f"Next {min(num_timestamps, len(next_datetimes))} timestamps "
|
874
|
+
+ f"for schedule '{schedule}':"
|
875
|
+
),
|
876
|
+
)
|
877
|
+
|
878
|
+
return True, "Success"
|
879
|
+
|
880
|
+
|
820
881
|
|
821
882
|
### NOTE: This must be the final statement of the module.
|
822
883
|
### Any subactions added below these lines will not
|
meerschaum/config/_version.py
CHANGED
@@ -36,7 +36,7 @@ class Daemon:
|
|
36
36
|
def __new__(
|
37
37
|
cls,
|
38
38
|
*args,
|
39
|
-
daemon_id
|
39
|
+
daemon_id: Optional[str] = None,
|
40
40
|
**kw
|
41
41
|
):
|
42
42
|
"""
|
@@ -129,7 +129,7 @@ class Daemon:
|
|
129
129
|
keep_daemon_output: bool, default True
|
130
130
|
If `False`, delete the daemon's output directory upon exiting.
|
131
131
|
|
132
|
-
allow_dirty_run :
|
132
|
+
allow_dirty_run, bool, default False:
|
133
133
|
If `True`, run the daemon, even if the `daemon_id` directory exists.
|
134
134
|
This option is dangerous because if the same `daemon_id` runs twice,
|
135
135
|
the last to finish will overwrite the output of the first.
|
@@ -138,7 +138,11 @@ class Daemon:
|
|
138
138
|
-------
|
139
139
|
Nothing — this will exit the parent process.
|
140
140
|
"""
|
141
|
-
import platform, sys, os
|
141
|
+
import platform, sys, os, traceback
|
142
|
+
from meerschaum.config._paths import LOGS_RESOURCES_PATH
|
143
|
+
from meerschaum.utils.warnings import warn
|
144
|
+
daemons_error_log_path = LOGS_RESOURCES_PATH / 'daemons_error.log'
|
145
|
+
|
142
146
|
daemon = attempt_import('daemon')
|
143
147
|
|
144
148
|
if platform.system() == 'Windows':
|
@@ -162,29 +166,37 @@ class Daemon:
|
|
162
166
|
log_refresh_seconds = get_config('jobs', 'logs', 'refresh_files_seconds')
|
163
167
|
self._log_refresh_timer = RepeatTimer(log_refresh_seconds, self.rotating_log.refresh_files)
|
164
168
|
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
self._log_refresh_timer.start()
|
171
|
-
result = self.target(*self.target_args, **self.target_kw)
|
172
|
-
self.properties['result'] = result
|
173
|
-
except Exception as e:
|
174
|
-
warn(e, stacklevel=3)
|
175
|
-
result = e
|
176
|
-
finally:
|
177
|
-
self._log_refresh_timer.cancel()
|
178
|
-
self.rotating_log.close()
|
179
|
-
if self.pid is None and self.pid_path.exists():
|
180
|
-
self.pid_path.unlink()
|
169
|
+
try:
|
170
|
+
with self._daemon_context:
|
171
|
+
try:
|
172
|
+
with open(self.pid_path, 'w+', encoding='utf-8') as f:
|
173
|
+
f.write(str(os.getpid()))
|
181
174
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
175
|
+
self._log_refresh_timer.start()
|
176
|
+
result = self.target(*self.target_args, **self.target_kw)
|
177
|
+
self.properties['result'] = result
|
178
|
+
except Exception as e:
|
179
|
+
warn(e, stacklevel=3)
|
180
|
+
result = e
|
181
|
+
finally:
|
182
|
+
self._log_refresh_timer.cancel()
|
183
|
+
self.rotating_log.close()
|
184
|
+
if self.pid is None and self.pid_path.exists():
|
185
|
+
self.pid_path.unlink()
|
186
|
+
|
187
|
+
if keep_daemon_output:
|
188
|
+
self._capture_process_timestamp('ended')
|
189
|
+
else:
|
190
|
+
self.cleanup()
|
191
|
+
|
192
|
+
return result
|
193
|
+
except Exception as e:
|
194
|
+
daemon_error = traceback.format_exc()
|
195
|
+
with open(LOGS_RESOURCES_PATH, 'a+', encoding='utf-8') as f:
|
196
|
+
f.write(daemon_error)
|
186
197
|
|
187
|
-
|
198
|
+
if daemon_error:
|
199
|
+
warn("Encountered an error while starting the daemon '{self}':\n{daemon_error}")
|
188
200
|
|
189
201
|
|
190
202
|
def _capture_process_timestamp(
|
@@ -452,10 +464,10 @@ class Daemon:
|
|
452
464
|
if daemon_context is not None:
|
453
465
|
daemon_context.close()
|
454
466
|
|
455
|
-
|
467
|
+
self.rotating_log.stop_log_fd_interception()
|
456
468
|
|
457
|
-
|
458
|
-
|
469
|
+
_close_pools()
|
470
|
+
raise SystemExit(0)
|
459
471
|
|
460
472
|
|
461
473
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
@@ -471,9 +483,10 @@ class Daemon:
|
|
471
483
|
if daemon_context is not None:
|
472
484
|
daemon_context.close()
|
473
485
|
|
474
|
-
|
486
|
+
self.rotating_log.stop_log_fd_interception()
|
475
487
|
|
476
|
-
|
488
|
+
_close_pools()
|
489
|
+
raise SystemExit(1)
|
477
490
|
|
478
491
|
|
479
492
|
def _send_signal(
|
@@ -650,7 +663,11 @@ class Daemon:
|
|
650
663
|
if '_rotating_log' in self.__dict__:
|
651
664
|
return self._rotating_log
|
652
665
|
|
653
|
-
self._rotating_log = RotatingFile(
|
666
|
+
self._rotating_log = RotatingFile(
|
667
|
+
self.log_path,
|
668
|
+
redirect_streams = True,
|
669
|
+
write_timestamps = True,
|
670
|
+
)
|
654
671
|
return self._rotating_log
|
655
672
|
|
656
673
|
|
@@ -663,6 +680,7 @@ class Daemon:
|
|
663
680
|
self.rotating_log.file_path,
|
664
681
|
num_files_to_keep = self.rotating_log.num_files_to_keep,
|
665
682
|
max_file_size = self.rotating_log.max_file_size,
|
683
|
+
write_timestamps = True,
|
666
684
|
)
|
667
685
|
return new_rotating_log.read()
|
668
686
|
|
@@ -714,7 +732,7 @@ class Daemon:
|
|
714
732
|
if not self.pid_path.exists():
|
715
733
|
return None
|
716
734
|
try:
|
717
|
-
with open(self.pid_path, 'r') as f:
|
735
|
+
with open(self.pid_path, 'r', encoding='utf-8') as f:
|
718
736
|
text = f.read()
|
719
737
|
pid = int(text.rstrip())
|
720
738
|
except Exception as e:
|
@@ -815,7 +833,7 @@ class Daemon:
|
|
815
833
|
if self.properties is not None:
|
816
834
|
try:
|
817
835
|
self.path.mkdir(parents=True, exist_ok=True)
|
818
|
-
with open(self.properties_path, 'w+') as properties_file:
|
836
|
+
with open(self.properties_path, 'w+', encoding='utf-8') as properties_file:
|
819
837
|
json.dump(self.properties, properties_file)
|
820
838
|
success, msg = True, 'Success'
|
821
839
|
except Exception as e:
|
@@ -0,0 +1,60 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# vim:fenc=utf-8
|
4
|
+
|
5
|
+
"""
|
6
|
+
Intercept OS-level file descriptors.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
from datetime import datetime
|
11
|
+
from meerschaum.utils.typing import Callable
|
12
|
+
|
13
|
+
class FileDescriptorInterceptor:
|
14
|
+
"""
|
15
|
+
A management class to intercept data written to a file descriptor.
|
16
|
+
"""
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
file_descriptor: int,
|
20
|
+
injection_hook: Callable[[], str],
|
21
|
+
):
|
22
|
+
"""
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
file_descriptor: int
|
26
|
+
The OS file descriptor from which to read.
|
27
|
+
|
28
|
+
injection_hook: Callable[[], str]
|
29
|
+
A callable which returns a string to be injected into the written data.
|
30
|
+
"""
|
31
|
+
self.injection_hook = injection_hook
|
32
|
+
self.original_file_descriptor = file_descriptor
|
33
|
+
self.new_file_descriptor = os.dup(file_descriptor)
|
34
|
+
self.read_pipe, self.write_pipe = os.pipe()
|
35
|
+
os.dup2(self.write_pipe, file_descriptor)
|
36
|
+
|
37
|
+
def start_interception(self):
|
38
|
+
"""
|
39
|
+
Read from the file descriptor and write the modified data after injection.
|
40
|
+
|
41
|
+
NOTE: This is blocking and is meant to be run in a thread.
|
42
|
+
"""
|
43
|
+
while True:
|
44
|
+
data = os.read(self.read_pipe, 1024)
|
45
|
+
if not data:
|
46
|
+
break
|
47
|
+
injected_str = self.injection_hook()
|
48
|
+
modified_data = data.replace(b'\n', f'\n{injected_str}'.encode('utf-8'))
|
49
|
+
os.write(self.new_file_descriptor, modified_data)
|
50
|
+
|
51
|
+
def stop_interception(self):
|
52
|
+
"""
|
53
|
+
Restore the file descriptors and close the new pipes.
|
54
|
+
"""
|
55
|
+
try:
|
56
|
+
os.dup2(self.new_file_descriptor, self.original_file_descriptor)
|
57
|
+
os.close(self.read_pipe)
|
58
|
+
os.close(self.write_pipe)
|
59
|
+
except OSError:
|
60
|
+
pass
|
@@ -13,9 +13,13 @@ import pathlib
|
|
13
13
|
import traceback
|
14
14
|
import sys
|
15
15
|
import atexit
|
16
|
+
from datetime import datetime, timezone, timedelta
|
16
17
|
from typing import List, Union, Optional, Tuple
|
17
18
|
from meerschaum.config import get_config
|
18
19
|
from meerschaum.utils.warnings import warn
|
20
|
+
from meerschaum.utils.misc import round_time
|
21
|
+
from meerschaum.utils.daemon.FileDescriptorInterceptor import FileDescriptorInterceptor
|
22
|
+
from meerschaum.utils.threading import Thread
|
19
23
|
import meerschaum as mrsm
|
20
24
|
daemon = mrsm.attempt_import('daemon')
|
21
25
|
|
@@ -33,6 +37,8 @@ class RotatingFile(io.IOBase):
|
|
33
37
|
num_files_to_keep: Optional[int] = None,
|
34
38
|
max_file_size: Optional[int] = None,
|
35
39
|
redirect_streams: bool = False,
|
40
|
+
write_timestamps: bool = False,
|
41
|
+
timestamps_format: str = '%Y-%m-%d %H:%M | ',
|
36
42
|
):
|
37
43
|
"""
|
38
44
|
Create a file-like object which manages other files.
|
@@ -54,6 +60,9 @@ class RotatingFile(io.IOBase):
|
|
54
60
|
|
55
61
|
NOTE: Only set this to `True` if you are entering into a daemon context.
|
56
62
|
Doing so will redirect `sys.stdout` and `sys.stderr` into the log files.
|
63
|
+
|
64
|
+
write_timestamps: bool, default False
|
65
|
+
If `True`, prepend the current UTC timestamp to each line of the file.
|
57
66
|
"""
|
58
67
|
self.file_path = pathlib.Path(file_path)
|
59
68
|
if num_files_to_keep is None:
|
@@ -68,6 +77,8 @@ class RotatingFile(io.IOBase):
|
|
68
77
|
self.num_files_to_keep = num_files_to_keep
|
69
78
|
self.max_file_size = max_file_size
|
70
79
|
self.redirect_streams = redirect_streams
|
80
|
+
self.write_timestamps = write_timestamps
|
81
|
+
self.timestamps_format = timestamps_format
|
71
82
|
self.subfile_regex_pattern = re.compile(
|
72
83
|
r'^'
|
73
84
|
+ self.file_path.name
|
@@ -87,14 +98,34 @@ class RotatingFile(io.IOBase):
|
|
87
98
|
atexit.register(self.close)
|
88
99
|
|
89
100
|
|
101
|
+
|
90
102
|
def fileno(self):
|
91
103
|
"""
|
92
104
|
Return the file descriptor for the latest subfile.
|
93
105
|
"""
|
106
|
+
import inspect
|
107
|
+
stack = inspect.stack()
|
108
|
+
parent_level = stack[1]
|
109
|
+
parent_module = parent_level[0].f_globals.get('__file__')
|
110
|
+
# if parent_module.endswith('daemon.py'):
|
111
|
+
# self._monkey_patch_os_write()
|
94
112
|
self.refresh_files()
|
95
113
|
return self._current_file_obj.fileno()
|
96
114
|
|
97
115
|
|
116
|
+
def _monkey_patch_os_write(self):
|
117
|
+
import os
|
118
|
+
import sys
|
119
|
+
import pathlib
|
120
|
+
path = pathlib.Path('/home/bmeares/test1.log')
|
121
|
+
original_write = os.write
|
122
|
+
def intercept(*args, **kwargs):
|
123
|
+
with open(path, 'w', encoding='utf-8') as f:
|
124
|
+
f.write(str(args))
|
125
|
+
original_write(*args, **kwargs)
|
126
|
+
os.write = intercept
|
127
|
+
|
128
|
+
|
98
129
|
def get_latest_subfile_path(self) -> pathlib.Path:
|
99
130
|
"""
|
100
131
|
Return the path for the latest subfile to which to write into.
|
@@ -247,8 +278,10 @@ class RotatingFile(io.IOBase):
|
|
247
278
|
if is_first_run_with_logs or lost_latest_handle:
|
248
279
|
self._current_file_obj = open(latest_subfile_path, 'a+', encoding='utf-8')
|
249
280
|
if self.redirect_streams:
|
281
|
+
self.stop_log_fd_interception()
|
250
282
|
daemon.daemon.redirect_stream(sys.stdout, self._current_file_obj)
|
251
283
|
daemon.daemon.redirect_stream(sys.stderr, self._current_file_obj)
|
284
|
+
self.start_log_fd_interception()
|
252
285
|
|
253
286
|
create_new_file = (
|
254
287
|
(latest_subfile_index == -1)
|
@@ -269,9 +302,11 @@ class RotatingFile(io.IOBase):
|
|
269
302
|
if self._previous_file_obj is not None:
|
270
303
|
if self.redirect_streams:
|
271
304
|
self._redirected_subfile_objects[old_subfile_index] = self._previous_file_obj
|
305
|
+
self.stop_log_fd_interception()
|
272
306
|
daemon.daemon.redirect_stream(self._previous_file_obj, self._current_file_obj)
|
273
307
|
daemon.daemon.redirect_stream(sys.stdout, self._current_file_obj)
|
274
308
|
daemon.daemon.redirect_stream(sys.stderr, self._current_file_obj)
|
309
|
+
self.start_log_fd_interception()
|
275
310
|
self.close(unused_only=True)
|
276
311
|
|
277
312
|
### Sanity check in case writing somehow fails.
|
@@ -279,6 +314,8 @@ class RotatingFile(io.IOBase):
|
|
279
314
|
self._previous_file_obj is None
|
280
315
|
|
281
316
|
self.delete(unused_only=True)
|
317
|
+
|
318
|
+
|
282
319
|
return self._current_file_obj
|
283
320
|
|
284
321
|
|
@@ -311,6 +348,13 @@ class RotatingFile(io.IOBase):
|
|
311
348
|
self._current_file_obj = None
|
312
349
|
|
313
350
|
|
351
|
+
def get_timestamp_prefix_str(self) -> str:
|
352
|
+
"""
|
353
|
+
Return the current minute prefixm string.
|
354
|
+
"""
|
355
|
+
return datetime.now(timezone.utc).strftime(self.timestamps_format)
|
356
|
+
|
357
|
+
|
314
358
|
def write(self, data: str) -> None:
|
315
359
|
"""
|
316
360
|
Write the given text into the latest subfile.
|
@@ -325,9 +369,15 @@ class RotatingFile(io.IOBase):
|
|
325
369
|
if isinstance(data, bytes):
|
326
370
|
data = data.decode('utf-8')
|
327
371
|
|
328
|
-
self.
|
372
|
+
prefix_str = self.get_timestamp_prefix_str() if self.write_timestamps else ""
|
373
|
+
suffix_str = "\n" if self.write_timestamps else ""
|
374
|
+
self.refresh_files(potential_new_len=len(prefix_str + data + suffix_str))
|
329
375
|
try:
|
376
|
+
if prefix_str:
|
377
|
+
self._current_file_obj.write(prefix_str)
|
330
378
|
self._current_file_obj.write(data)
|
379
|
+
if suffix_str:
|
380
|
+
self._current_file_obj.write(suffix_str)
|
331
381
|
except Exception as e:
|
332
382
|
warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
|
333
383
|
self.flush()
|
@@ -471,7 +521,7 @@ class RotatingFile(io.IOBase):
|
|
471
521
|
subfile_object = self.subfile_objects[subfile_index]
|
472
522
|
for i in range(self.SEEK_BACK_ATTEMPTS):
|
473
523
|
try:
|
474
|
-
subfile_object.seek(max(seek_ix - i), 0)
|
524
|
+
subfile_object.seek(max((seek_ix - i), 0))
|
475
525
|
subfile_lines = subfile_object.readlines()
|
476
526
|
except UnicodeDecodeError:
|
477
527
|
continue
|
@@ -538,6 +588,43 @@ class RotatingFile(io.IOBase):
|
|
538
588
|
sys.stderr.flush()
|
539
589
|
|
540
590
|
|
591
|
+
def start_log_fd_interception(self):
|
592
|
+
"""
|
593
|
+
Start the file descriptor monitoring threads.
|
594
|
+
"""
|
595
|
+
self._stdout_interceptor = FileDescriptorInterceptor(
|
596
|
+
sys.stdout.fileno(),
|
597
|
+
self.get_timestamp_prefix_str,
|
598
|
+
)
|
599
|
+
self._stderr_interceptor = FileDescriptorInterceptor(
|
600
|
+
sys.stderr.fileno(),
|
601
|
+
self.get_timestamp_prefix_str,
|
602
|
+
)
|
603
|
+
self._stdout_interceptor_thread = Thread(target=self._stdout_interceptor.start_interception)
|
604
|
+
self._stderr_interceptor_thread = Thread(target=self._stderr_interceptor.start_interception)
|
605
|
+
self._stdout_interceptor_thread.start()
|
606
|
+
self._stderr_interceptor_thread.start()
|
607
|
+
|
608
|
+
|
609
|
+
def stop_log_fd_interception(self):
|
610
|
+
"""
|
611
|
+
Stop the file descriptor monitoring threads.
|
612
|
+
"""
|
613
|
+
stdout_interceptor = self.__dict__.get('_stdout_interceptor', None)
|
614
|
+
stderr_interceptor = self.__dict__.get('_stderr_interceptor', None)
|
615
|
+
stdout_interceptor_thread = self.__dict__.get('_stdout_interceptor_thread', None)
|
616
|
+
stderr_interceptor_thread = self.__dict__.get('_stderr_interceptor_thread', None)
|
617
|
+
if stdout_interceptor is None:
|
618
|
+
return
|
619
|
+
stdout_interceptor.stop_interception()
|
620
|
+
stderr_interceptor.stop_interception()
|
621
|
+
try:
|
622
|
+
stdout_interceptor_thread.join()
|
623
|
+
stderr_interceptor_thread.join()
|
624
|
+
except Exception:
|
625
|
+
pass
|
626
|
+
|
627
|
+
|
541
628
|
def __repr__(self) -> str:
|
542
629
|
"""
|
543
630
|
Return basic info for this `RotatingFile`.
|
@@ -35,6 +35,7 @@ _locks = {
|
|
35
35
|
}
|
36
36
|
_checked_for_updates = set()
|
37
37
|
_is_installed_first_check: Dict[str, bool] = {}
|
38
|
+
_MRSM_PACKAGE_ARCHIVES_PREFIX: str = "https://meerschaum.io/files/archives/"
|
38
39
|
|
39
40
|
def get_module_path(
|
40
41
|
import_name: str,
|
@@ -640,9 +641,15 @@ def need_update(
|
|
640
641
|
|
641
642
|
### We might be depending on a prerelease.
|
642
643
|
### Sanity check that the required version is not greater than the installed version.
|
644
|
+
required_version = (
|
645
|
+
required_version.replace(_MRSM_PACKAGE_ARCHIVES_PREFIX, '')
|
646
|
+
.replace(' @ ', '').replace('wheels', '').replace('+mrsm', '').replace('/-', '')
|
647
|
+
.replace('-py3-none-any.whl', '')
|
648
|
+
)
|
649
|
+
|
643
650
|
if 'a' in required_version:
|
644
|
-
required_version = required_version.replace('a', '-dev')
|
645
|
-
version = version.replace('a', '-dev')
|
651
|
+
required_version = required_version.replace('a', '-dev').replace('+mrsm', '')
|
652
|
+
version = version.replace('a', '-dev').replace('+mrsm', '')
|
646
653
|
try:
|
647
654
|
return (
|
648
655
|
(not semver.Version.parse(version).match(required_version))
|
@@ -49,10 +49,10 @@ packages: Dict[str, Dict[str, str]] = {
|
|
49
49
|
'daemon' : 'python-daemon>=0.2.3',
|
50
50
|
'fasteners' : 'fasteners>=0.18.0',
|
51
51
|
'psutil' : 'psutil>=5.8.0',
|
52
|
-
'
|
52
|
+
'watchfiles' : 'watchfiles>=0.21.0',
|
53
53
|
'dill' : 'dill>=0.3.3',
|
54
54
|
'virtualenv' : 'virtualenv>=20.1.0',
|
55
|
-
'apscheduler' : '
|
55
|
+
'apscheduler' : 'APScheduler>=4.0.0a5',
|
56
56
|
},
|
57
57
|
'drivers': {
|
58
58
|
'cryptography' : 'cryptography>=38.0.1',
|
@@ -89,6 +89,7 @@ packages: Dict[str, Dict[str, str]] = {
|
|
89
89
|
'pytest' : 'pytest>=6.2.2',
|
90
90
|
'pytest_xdist' : 'pytest-xdist>=3.2.1',
|
91
91
|
'heartrate' : 'heartrate>=0.2.1',
|
92
|
+
'build' : 'build>=1.2.1',
|
92
93
|
},
|
93
94
|
'setup': {
|
94
95
|
},
|
@@ -149,7 +150,6 @@ packages['api'] = {
|
|
149
150
|
'passlib' : 'passlib>=1.7.4',
|
150
151
|
'fastapi_login' : 'fastapi-login>=1.7.2',
|
151
152
|
'multipart' : 'python-multipart>=0.0.5',
|
152
|
-
# 'pydantic' : 'pydantic>2.0.0',
|
153
153
|
'httpx' : 'httpx>=0.24.1',
|
154
154
|
'websockets' : 'websockets>=11.0.3',
|
155
155
|
}
|
meerschaum/utils/schedule.py
CHANGED
@@ -12,7 +12,8 @@ from datetime import datetime, timezone, timedelta, timedelta
|
|
12
12
|
import meerschaum as mrsm
|
13
13
|
from meerschaum.utils.typing import Callable, Any, Optional, List, Dict
|
14
14
|
|
15
|
-
|
15
|
+
STARTING_KEYWORD: str = 'starting'
|
16
|
+
INTERVAL_UNITS: List[str] = ['months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'years']
|
16
17
|
FREQUENCY_ALIASES: Dict[str, str] = {
|
17
18
|
'daily': 'every 1 day',
|
18
19
|
'hourly': 'every 1 hour',
|
@@ -20,6 +21,7 @@ FREQUENCY_ALIASES: Dict[str, str] = {
|
|
20
21
|
'weekly': 'every 1 week',
|
21
22
|
'monthly': 'every 1 month',
|
22
23
|
'secondly': 'every 1 second',
|
24
|
+
'yearly': 'every 1 year',
|
23
25
|
}
|
24
26
|
LOGIC_ALIASES: Dict[str, str] = {
|
25
27
|
'and': '&',
|
@@ -27,7 +29,7 @@ LOGIC_ALIASES: Dict[str, str] = {
|
|
27
29
|
' through ': '-',
|
28
30
|
' thru ': '-',
|
29
31
|
' - ': '-',
|
30
|
-
'beginning':
|
32
|
+
'beginning': STARTING_KEYWORD,
|
31
33
|
}
|
32
34
|
CRON_DAYS_OF_WEEK: List[str] = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
|
33
35
|
CRON_DAYS_OF_WEEK_ALIASES: Dict[str, str] = {
|
@@ -65,8 +67,8 @@ SCHEDULE_ALIASES: Dict[str, str] = {
|
|
65
67
|
**CRON_DAYS_OF_WEEK_ALIASES,
|
66
68
|
**CRON_MONTHS_ALIASES,
|
67
69
|
}
|
68
|
-
STARTING_KEYWORD: str = 'starting'
|
69
70
|
|
71
|
+
_scheduler = None
|
70
72
|
def schedule_function(
|
71
73
|
function: Callable[[Any], Any],
|
72
74
|
schedule: str,
|
@@ -87,23 +89,35 @@ def schedule_function(
|
|
87
89
|
The frequency schedule at which `function` should be executed (e.g. `'daily'`).
|
88
90
|
|
89
91
|
"""
|
90
|
-
import
|
92
|
+
import asyncio
|
91
93
|
from meerschaum.utils.warnings import warn
|
92
94
|
from meerschaum.utils.misc import filter_keywords, round_time
|
95
|
+
global _scheduler
|
93
96
|
kw['debug'] = debug
|
94
97
|
kw = filter_keywords(function, **kw)
|
95
98
|
|
96
99
|
apscheduler = mrsm.attempt_import('apscheduler', lazy=False)
|
97
100
|
now = round_time(datetime.now(timezone.utc), timedelta(minutes=1))
|
98
101
|
trigger = parse_schedule(schedule, now=now)
|
102
|
+
_scheduler = apscheduler.AsyncScheduler()
|
103
|
+
try:
|
104
|
+
loop = asyncio.get_running_loop()
|
105
|
+
except RuntimeError:
|
106
|
+
loop = asyncio.new_event_loop()
|
107
|
+
|
108
|
+
async def run_scheduler():
|
109
|
+
async with _scheduler:
|
110
|
+
job = await _scheduler.add_schedule(function, trigger, args=args, kwargs=kw)
|
111
|
+
try:
|
112
|
+
await _scheduler.run_until_stopped()
|
113
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
114
|
+
await _stop_scheduler()
|
115
|
+
raise e
|
99
116
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
except KeyboardInterrupt as e:
|
105
|
-
scheduler.stop()
|
106
|
-
scheduler.wait_until_stopped()
|
117
|
+
try:
|
118
|
+
loop.run_until_complete(run_scheduler())
|
119
|
+
except (KeyboardInterrupt, SystemExit) as e:
|
120
|
+
loop.run_until_complete(_stop_scheduler())
|
107
121
|
|
108
122
|
|
109
123
|
def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
@@ -134,7 +148,7 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
134
148
|
|
135
149
|
### TODO Allow for combining `and` + `or` logic.
|
136
150
|
if '&' in schedule and '|' in schedule:
|
137
|
-
|
151
|
+
raise ValueError(f"Cannot accept both 'and' + 'or' logic in the schedule frequency.")
|
138
152
|
|
139
153
|
join_str = '|' if '|' in schedule else '&'
|
140
154
|
join_trigger = (
|
@@ -152,12 +166,6 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
152
166
|
|
153
167
|
has_seconds = 'second' in schedule
|
154
168
|
has_minutes = 'minute' in schedule
|
155
|
-
has_days = 'day' in schedule
|
156
|
-
has_weeks = 'week' in schedule
|
157
|
-
has_hours = 'hour' in schedule
|
158
|
-
num_hourly_intervals = schedule.count('hour')
|
159
|
-
divided_days = False
|
160
|
-
divided_hours = False
|
161
169
|
|
162
170
|
for schedule_part in schedule_parts:
|
163
171
|
|
@@ -168,10 +176,9 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
168
176
|
)
|
169
177
|
schedule_unit = schedule_unit.rstrip('s') + 's'
|
170
178
|
if schedule_unit not in INTERVAL_UNITS:
|
171
|
-
|
179
|
+
raise ValueError(
|
172
180
|
f"Invalid interval '{schedule_unit}'.\n"
|
173
|
-
+ f" Accepted values are {items_str(INTERVAL_UNITS)}."
|
174
|
-
ValueError,
|
181
|
+
+ f" Accepted values are {items_str(INTERVAL_UNITS)}."
|
175
182
|
)
|
176
183
|
|
177
184
|
schedule_num = (
|
@@ -180,29 +187,6 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
180
187
|
else float(schedule_num_str)
|
181
188
|
)
|
182
189
|
|
183
|
-
### NOTE: When combining days or weeks with other schedules,
|
184
|
-
### we must divide one of the day-schedules by 2.
|
185
|
-
### TODO Remove this when APScheduler is patched.
|
186
|
-
if (
|
187
|
-
join_str == '&'
|
188
|
-
and (has_days or has_weeks)
|
189
|
-
and len(schedule_parts) > 1
|
190
|
-
and not divided_days
|
191
|
-
):
|
192
|
-
schedule_num /= 2
|
193
|
-
divided_days = True
|
194
|
-
|
195
|
-
### NOTE: When combining multiple hourly intervals,
|
196
|
-
### one must be divided by 2.
|
197
|
-
if (
|
198
|
-
join_str == '&'
|
199
|
-
# and num_hourly_intervals > 1
|
200
|
-
and len(schedule_parts) > 1
|
201
|
-
and not divided_hours
|
202
|
-
):
|
203
|
-
schedule_num /= 2
|
204
|
-
# divided_hours = True
|
205
|
-
|
206
190
|
trigger = (
|
207
191
|
apscheduler_triggers_interval.IntervalTrigger(
|
208
192
|
**{
|
@@ -210,12 +194,12 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
210
194
|
'start_time': starting_ts,
|
211
195
|
}
|
212
196
|
)
|
213
|
-
if schedule_unit
|
197
|
+
if schedule_unit not in ('months', 'years') else (
|
214
198
|
apscheduler_triggers_calendarinterval.CalendarIntervalTrigger(
|
215
199
|
**{
|
216
200
|
schedule_unit: schedule_num,
|
217
201
|
'start_date': starting_ts,
|
218
|
-
|
202
|
+
'timezone': starting_ts.tzinfo,
|
219
203
|
}
|
220
204
|
)
|
221
205
|
)
|
@@ -223,12 +207,15 @@ def parse_schedule(schedule: str, now: Optional[datetime] = None):
|
|
223
207
|
|
224
208
|
### Determine whether this is a pure cron string or a cron subset (e.g. 'may-aug')_.
|
225
209
|
else:
|
226
|
-
first_three_prefix = schedule_part[:3]
|
210
|
+
first_three_prefix = schedule_part[:3].lower()
|
211
|
+
first_four_prefix = schedule_part[:4].lower()
|
227
212
|
cron_kw = {}
|
228
213
|
if first_three_prefix in CRON_DAYS_OF_WEEK:
|
229
214
|
cron_kw['day_of_week'] = schedule_part
|
230
215
|
elif first_three_prefix in CRON_MONTHS:
|
231
216
|
cron_kw['month'] = schedule_part
|
217
|
+
elif is_int(first_four_prefix) and len(first_four_prefix) == 4:
|
218
|
+
cron_kw['year'] = int(first_four_prefix)
|
232
219
|
trigger = (
|
233
220
|
apscheduler_triggers_cron.CronTrigger(
|
234
221
|
**{
|
@@ -301,3 +288,10 @@ def parse_start_time(schedule: str, now: Optional[datetime] = None) -> datetime:
|
|
301
288
|
if not starting_ts.tzinfo:
|
302
289
|
starting_ts = starting_ts.replace(tzinfo=timezone.utc)
|
303
290
|
return starting_ts
|
291
|
+
|
292
|
+
|
293
|
+
async def _stop_scheduler():
|
294
|
+
if _scheduler is None:
|
295
|
+
return
|
296
|
+
await _scheduler.stop()
|
297
|
+
await _scheduler.wait_until_stopped()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: meerschaum
|
3
|
-
Version: 2.2.
|
3
|
+
Version: 2.2.0rc1
|
4
4
|
Summary: Sync Time-Series Pipes with Meerschaum
|
5
5
|
Home-page: https://meerschaum.io
|
6
6
|
Author: Bennett Meares
|
@@ -53,10 +53,10 @@ Requires-Dist: more-itertools >=8.7.0 ; extra == '_required'
|
|
53
53
|
Requires-Dist: python-daemon >=0.2.3 ; extra == '_required'
|
54
54
|
Requires-Dist: fasteners >=0.18.0 ; extra == '_required'
|
55
55
|
Requires-Dist: psutil >=5.8.0 ; extra == '_required'
|
56
|
-
Requires-Dist:
|
56
|
+
Requires-Dist: watchfiles >=0.21.0 ; extra == '_required'
|
57
57
|
Requires-Dist: dill >=0.3.3 ; extra == '_required'
|
58
58
|
Requires-Dist: virtualenv >=20.1.0 ; extra == '_required'
|
59
|
-
Requires-Dist:
|
59
|
+
Requires-Dist: APScheduler >=4.0.0a5 ; extra == '_required'
|
60
60
|
Provides-Extra: api
|
61
61
|
Requires-Dist: uvicorn[standard] >=0.22.0 ; extra == 'api'
|
62
62
|
Requires-Dist: gunicorn >=20.1.0 ; extra == 'api'
|
@@ -102,10 +102,10 @@ Requires-Dist: more-itertools >=8.7.0 ; extra == 'api'
|
|
102
102
|
Requires-Dist: python-daemon >=0.2.3 ; extra == 'api'
|
103
103
|
Requires-Dist: fasteners >=0.18.0 ; extra == 'api'
|
104
104
|
Requires-Dist: psutil >=5.8.0 ; extra == 'api'
|
105
|
-
Requires-Dist:
|
105
|
+
Requires-Dist: watchfiles >=0.21.0 ; extra == 'api'
|
106
106
|
Requires-Dist: dill >=0.3.3 ; extra == 'api'
|
107
107
|
Requires-Dist: virtualenv >=20.1.0 ; extra == 'api'
|
108
|
-
Requires-Dist:
|
108
|
+
Requires-Dist: APScheduler >=4.0.0a5 ; extra == 'api'
|
109
109
|
Requires-Dist: pprintpp >=0.4.0 ; extra == 'api'
|
110
110
|
Requires-Dist: asciitree >=0.3.3 ; extra == 'api'
|
111
111
|
Requires-Dist: typing-extensions >=4.7.1 ; extra == 'api'
|
@@ -148,6 +148,7 @@ Requires-Dist: mypy >=0.812.0 ; extra == 'dev-tools'
|
|
148
148
|
Requires-Dist: pytest >=6.2.2 ; extra == 'dev-tools'
|
149
149
|
Requires-Dist: pytest-xdist >=3.2.1 ; extra == 'dev-tools'
|
150
150
|
Requires-Dist: heartrate >=0.2.1 ; extra == 'dev-tools'
|
151
|
+
Requires-Dist: build >=1.2.1 ; extra == 'dev-tools'
|
151
152
|
Provides-Extra: docs
|
152
153
|
Requires-Dist: mkdocs >=1.1.2 ; extra == 'docs'
|
153
154
|
Requires-Dist: mkdocs-material >=6.2.5 ; extra == 'docs'
|
@@ -208,10 +209,10 @@ Requires-Dist: more-itertools >=8.7.0 ; extra == 'full'
|
|
208
209
|
Requires-Dist: python-daemon >=0.2.3 ; extra == 'full'
|
209
210
|
Requires-Dist: fasteners >=0.18.0 ; extra == 'full'
|
210
211
|
Requires-Dist: psutil >=5.8.0 ; extra == 'full'
|
211
|
-
Requires-Dist:
|
212
|
+
Requires-Dist: watchfiles >=0.21.0 ; extra == 'full'
|
212
213
|
Requires-Dist: dill >=0.3.3 ; extra == 'full'
|
213
214
|
Requires-Dist: virtualenv >=20.1.0 ; extra == 'full'
|
214
|
-
Requires-Dist:
|
215
|
+
Requires-Dist: APScheduler >=4.0.0a5 ; extra == 'full'
|
215
216
|
Requires-Dist: cryptography >=38.0.1 ; extra == 'full'
|
216
217
|
Requires-Dist: psycopg[binary] >=3.1.18 ; extra == 'full'
|
217
218
|
Requires-Dist: PyMySQL >=0.9.0 ; extra == 'full'
|
@@ -292,10 +293,10 @@ Requires-Dist: more-itertools >=8.7.0 ; extra == 'sql'
|
|
292
293
|
Requires-Dist: python-daemon >=0.2.3 ; extra == 'sql'
|
293
294
|
Requires-Dist: fasteners >=0.18.0 ; extra == 'sql'
|
294
295
|
Requires-Dist: psutil >=5.8.0 ; extra == 'sql'
|
295
|
-
Requires-Dist:
|
296
|
+
Requires-Dist: watchfiles >=0.21.0 ; extra == 'sql'
|
296
297
|
Requires-Dist: dill >=0.3.3 ; extra == 'sql'
|
297
298
|
Requires-Dist: virtualenv >=20.1.0 ; extra == 'sql'
|
298
|
-
Requires-Dist:
|
299
|
+
Requires-Dist: APScheduler >=4.0.0a5 ; extra == 'sql'
|
299
300
|
Provides-Extra: stack
|
300
301
|
Requires-Dist: docker-compose >=1.29.2 ; extra == 'stack'
|
301
302
|
|
@@ -1,7 +1,7 @@
|
|
1
1
|
meerschaum/__init__.py,sha256=mw_PhxT7155SW8d_S51T4-WTibL5g205CwMa3qX8qyA,1487
|
2
2
|
meerschaum/__main__.py,sha256=8XGS2sJCtz6AMfBv5vm84mH1eS36h1sN1eL8SSmYtE4,2763
|
3
3
|
meerschaum/_internal/__init__.py,sha256=ilC7utfKtin7GAvuN34fKyUQYfPyqH0Mm3MJF5iyEf4,169
|
4
|
-
meerschaum/_internal/entry.py,sha256=
|
4
|
+
meerschaum/_internal/entry.py,sha256=Oga0OH808UtTJGvvkQQFRKjVCjSCRYjJe3M1A9-yf1k,4806
|
5
5
|
meerschaum/_internal/arguments/__init__.py,sha256=HFciFQgo2ZOT19Mo6CpLhPYlpLYh2sNn1C9Lo7NMADc,519
|
6
6
|
meerschaum/_internal/arguments/_parse_arguments.py,sha256=dNVDBvXYNgEw-TrlZJ9A6VPlG696EpQcQm6FOAhseqw,10249
|
7
7
|
meerschaum/_internal/arguments/_parser.py,sha256=OtcrZK-_qV9a5qpdcP9NLKOGRevjmCU9fBLbB88px3c,13719
|
@@ -38,7 +38,7 @@ meerschaum/actions/register.py,sha256=l_21LWZCzKwJVex_xAXECC5WVW1VEuIX9HSp7CuyCw
|
|
38
38
|
meerschaum/actions/reload.py,sha256=gMXeFBGVfyQ7uhKhYf6bLaDMD0fLPcA9BrLBSiuvWIc,508
|
39
39
|
meerschaum/actions/setup.py,sha256=KkAGWcgwzl_L6A19fTmTX1KtBjW2FwD8QenLjPy0mQQ,3205
|
40
40
|
meerschaum/actions/sh.py,sha256=fLfTJaacKu4sjLTRqEzzYlT2WbbdZBEczsKb6F-qAek,2026
|
41
|
-
meerschaum/actions/show.py,sha256=
|
41
|
+
meerschaum/actions/show.py,sha256=MtPp6PNjbQiMcxqsfvU7_0rGvB8Z59-TURmEwvpvT6I,28057
|
42
42
|
meerschaum/actions/sql.py,sha256=wYofwk1vGO96U2ncigGEfMtYMZeprz2FR1PRRZhkAPI,4311
|
43
43
|
meerschaum/actions/stack.py,sha256=WMRMebyYwZGNlbnj6Ja09qvCSDNteFJOTa8_joHlnVo,5886
|
44
44
|
meerschaum/actions/start.py,sha256=mNFWqxc_o9moavvDQWE4YoZF6b-SW2nKyw5MtwIj-90,18384
|
@@ -134,7 +134,7 @@ meerschaum/config/_preprocess.py,sha256=-AEA8m_--KivZwTQ1sWN6LTn5sio_fUr2XZ51BO6
|
|
134
134
|
meerschaum/config/_read_config.py,sha256=WFZKIXZMDe_ca0ES7ivgM_mnwShvFxLdoeisT_X5-h0,14720
|
135
135
|
meerschaum/config/_shell.py,sha256=s74cmJl8NrhM_Y1cB_P41_JDUYXV0g4WXnKFZWMtnrY,3551
|
136
136
|
meerschaum/config/_sync.py,sha256=Q-sz5YcjL3CJS2Dyw4rVRQsz9th9GWa9o5F9D0Jrmn8,4120
|
137
|
-
meerschaum/config/_version.py,sha256=
|
137
|
+
meerschaum/config/_version.py,sha256=N7N8FP8jZtOySQsoOturqBbQHE9lVrxPySO9oQt-NRo,74
|
138
138
|
meerschaum/config/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
139
139
|
meerschaum/config/stack/__init__.py,sha256=pKR7aDqqrGZjjNhbWbA9AMdfBjF_-zl7xtgVXk9B9Mg,9012
|
140
140
|
meerschaum/config/stack/grafana/__init__.py,sha256=wzuoch_AK49lcn7lH2qTSJ_PPbSagF4lcweeipz_XiE,2010
|
@@ -204,14 +204,15 @@ meerschaum/utils/networking.py,sha256=Sr_eYUGW8_UV9-k9LqRFf7xLtbUcsDucODyLCRsFRU
|
|
204
204
|
meerschaum/utils/pool.py,sha256=vkE42af4fjrTEJTxf6Ek3xGucm1MtEkpsSEiaVzNKHs,2655
|
205
205
|
meerschaum/utils/process.py,sha256=tbEutHAg_Kn5UetOI-fduRjsafGOYX5tkLvpzqosgvc,7098
|
206
206
|
meerschaum/utils/prompt.py,sha256=0mBFbgi_l9rCou9UnC_6qKTHkqyl1Z_jSRzfmc0xRXM,16490
|
207
|
-
meerschaum/utils/schedule.py,sha256=
|
207
|
+
meerschaum/utils/schedule.py,sha256=nXiOAHNq51WDS5NS4MPMDmv1MY7RV0TxRNXN69S_w1w,10020
|
208
208
|
meerschaum/utils/sql.py,sha256=4sCNEpgUd6uFz6ySs4nnUMVaOT0YAvPM1ZlQYJTSF-0,46656
|
209
209
|
meerschaum/utils/threading.py,sha256=fAXk7-FnbFvdU1FQ4vHKk5NeGbbTpTw7y9dRnlVayNI,2472
|
210
210
|
meerschaum/utils/typing.py,sha256=L05wOXfWdn_nJ0KnZVr-2zdMYcqjdyOW_7InT3xe6-s,2807
|
211
211
|
meerschaum/utils/warnings.py,sha256=0b5O2DBbhEAGnu6RAB1hlHSVmwL_hcR3EiMkExXmBJ0,6535
|
212
212
|
meerschaum/utils/yaml.py,sha256=vbCrFjdapKsZ9wRRaI9Ih8dVUwZ-KHpSzfGhRcpDBgQ,3162
|
213
|
-
meerschaum/utils/daemon/Daemon.py,sha256=
|
214
|
-
meerschaum/utils/daemon/
|
213
|
+
meerschaum/utils/daemon/Daemon.py,sha256=pqUYMjuP08IW6nQ8Cg2g_RyP0LAYi1gAi67_3fahxJ0,33047
|
214
|
+
meerschaum/utils/daemon/FileDescriptorInterceptor.py,sha256=avmQa43HISIAIu6dfB0FL5tnrfaIBjfp5M8tRV0nFbo,1813
|
215
|
+
meerschaum/utils/daemon/RotatingFile.py,sha256=wKb2Q93msgc0RPQTHr-nJ1hOwQnEoNNURlLg6Ea83M8,22775
|
215
216
|
meerschaum/utils/daemon/__init__.py,sha256=7LZeLuakprirfQ_XjnMoOp5Z96cy83vs75oBJiXU19Q,8174
|
216
217
|
meerschaum/utils/daemon/_names.py,sha256=Prf7xA2GWDbKR_9Xq9_5RTTIf9GNWY3Yt0s4tEU3JgM,4330
|
217
218
|
meerschaum/utils/dtypes/__init__.py,sha256=JR9PViJTzhukZhq0QoPIs73HOnXZZr8OmfhAAD4OAUA,6261
|
@@ -221,16 +222,16 @@ meerschaum/utils/formatting/_jobs.py,sha256=s1lVcdMkzNj5Bqw-GsUhcguUFtahi5nQ-kg1
|
|
221
222
|
meerschaum/utils/formatting/_pipes.py,sha256=wy0iWJFsFl3X2VloaiA_gp9Yx9w6tD3FQZvAQAqef4A,19492
|
222
223
|
meerschaum/utils/formatting/_pprint.py,sha256=tgrT3FyGyu5CWJYysqK3kX1xdZYorlbOk9fcU_vt9Qg,3096
|
223
224
|
meerschaum/utils/formatting/_shell.py,sha256=ox75O7VHDAiwzSvdMSJZhXLadvAqYJVeihU6WeZ2Ogc,3677
|
224
|
-
meerschaum/utils/packages/__init__.py,sha256=
|
225
|
-
meerschaum/utils/packages/_packages.py,sha256=
|
225
|
+
meerschaum/utils/packages/__init__.py,sha256=Ohwzw1GufuqQd-N2fkPSCXXGaSSDKB5_zVZ3S9atviM,57032
|
226
|
+
meerschaum/utils/packages/_packages.py,sha256=eSTwPNe2llLZFdBSofuKz903mxL7feNNUYb8YlkYjhQ,7968
|
226
227
|
meerschaum/utils/packages/lazy_loader.py,sha256=VHnph3VozH29R4JnSSBfwtA5WKZYZQFT_GeQSShCnuc,2540
|
227
228
|
meerschaum/utils/venv/_Venv.py,sha256=sBnlmxHdAh2bx8btfVoD79-H9-cYsv5lP02IIXkyECs,3553
|
228
229
|
meerschaum/utils/venv/__init__.py,sha256=sj-n8scWH2NPDJGAxfpqzsYqVUt2jMEr-7Uq9G7YUNQ,23183
|
229
|
-
meerschaum-2.2.
|
230
|
-
meerschaum-2.2.
|
231
|
-
meerschaum-2.2.
|
232
|
-
meerschaum-2.2.
|
233
|
-
meerschaum-2.2.
|
234
|
-
meerschaum-2.2.
|
235
|
-
meerschaum-2.2.
|
236
|
-
meerschaum-2.2.
|
230
|
+
meerschaum-2.2.0rc1.dist-info/LICENSE,sha256=jG2zQEdRNt88EgHUWPpXVWmOrOduUQRx7MnYV9YIPaw,11359
|
231
|
+
meerschaum-2.2.0rc1.dist-info/METADATA,sha256=78COJ__GnuF4XKU_sijVelE7Aj1KC0DxGf4RjOEz3Po,23964
|
232
|
+
meerschaum-2.2.0rc1.dist-info/NOTICE,sha256=OTA9Fcthjf5BRvWDDIcBC_xfLpeDV-RPZh3M-HQBRtQ,114
|
233
|
+
meerschaum-2.2.0rc1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
|
234
|
+
meerschaum-2.2.0rc1.dist-info/entry_points.txt,sha256=5YBVzibw-0rNA_1VjB16z5GABsOGf-CDhW4yqH8C7Gc,88
|
235
|
+
meerschaum-2.2.0rc1.dist-info/top_level.txt,sha256=bNoSiDj0El6buocix-FRoAtJOeq1qOF5rRm2u9i7Q6A,11
|
236
|
+
meerschaum-2.2.0rc1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
237
|
+
meerschaum-2.2.0rc1.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|