meerschaum 2.1.6__py3-none-any.whl → 2.2.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.
- meerschaum/__main__.py +1 -1
- meerschaum/_internal/arguments/_parser.py +3 -0
- meerschaum/_internal/entry.py +3 -2
- meerschaum/_internal/shell/Shell.py +1 -6
- meerschaum/actions/api.py +1 -1
- meerschaum/actions/install.py +7 -3
- meerschaum/actions/show.py +128 -42
- meerschaum/actions/sync.py +7 -3
- meerschaum/api/__init__.py +24 -14
- meerschaum/api/_oauth2.py +4 -4
- meerschaum/api/dash/callbacks/dashboard.py +93 -23
- meerschaum/api/dash/callbacks/jobs.py +55 -3
- meerschaum/api/dash/jobs.py +34 -8
- meerschaum/api/dash/keys.py +1 -1
- meerschaum/api/dash/pages/dashboard.py +14 -4
- meerschaum/api/dash/pipes.py +137 -26
- meerschaum/api/dash/plugins.py +25 -9
- meerschaum/api/resources/static/js/xterm.js +1 -1
- meerschaum/api/resources/templates/termpage.html +3 -0
- meerschaum/api/routes/_login.py +5 -4
- meerschaum/api/routes/_plugins.py +6 -3
- meerschaum/config/_dash.py +11 -0
- meerschaum/config/_default.py +3 -1
- meerschaum/config/_jobs.py +13 -4
- meerschaum/config/_paths.py +2 -0
- meerschaum/config/_shell.py +0 -1
- meerschaum/config/_sync.py +2 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +6 -7
- meerschaum/config/stack/grafana/__init__.py +1 -1
- meerschaum/config/static/__init__.py +4 -1
- meerschaum/connectors/__init__.py +2 -0
- meerschaum/connectors/api/_plugins.py +2 -1
- meerschaum/connectors/sql/SQLConnector.py +4 -2
- meerschaum/connectors/sql/_create_engine.py +9 -9
- meerschaum/connectors/sql/_fetch.py +8 -11
- meerschaum/connectors/sql/_instance.py +3 -1
- meerschaum/connectors/sql/_pipes.py +61 -39
- meerschaum/connectors/sql/_plugins.py +0 -2
- meerschaum/connectors/sql/_sql.py +7 -9
- meerschaum/core/Pipe/_dtypes.py +2 -1
- meerschaum/core/Pipe/_sync.py +26 -13
- meerschaum/core/User/_User.py +158 -16
- meerschaum/core/User/__init__.py +1 -1
- meerschaum/plugins/_Plugin.py +12 -3
- meerschaum/plugins/__init__.py +23 -1
- meerschaum/utils/daemon/Daemon.py +89 -36
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
- meerschaum/utils/daemon/RotatingFile.py +130 -14
- meerschaum/utils/daemon/__init__.py +3 -0
- meerschaum/utils/dataframe.py +183 -8
- meerschaum/utils/dtypes/__init__.py +9 -5
- meerschaum/utils/formatting/_pipes.py +44 -10
- meerschaum/utils/misc.py +34 -2
- meerschaum/utils/packages/__init__.py +25 -8
- meerschaum/utils/packages/_packages.py +18 -20
- meerschaum/utils/process.py +13 -10
- meerschaum/utils/schedule.py +276 -30
- meerschaum/utils/threading.py +1 -0
- meerschaum/utils/typing.py +1 -1
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/RECORD +68 -66
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
meerschaum/core/Pipe/_sync.py
CHANGED
@@ -12,6 +12,7 @@ import json
|
|
12
12
|
import time
|
13
13
|
import threading
|
14
14
|
import multiprocessing
|
15
|
+
import functools
|
15
16
|
from datetime import datetime, timedelta
|
16
17
|
|
17
18
|
from meerschaum.utils.typing import (
|
@@ -518,6 +519,8 @@ def exists(
|
|
518
519
|
def filter_existing(
|
519
520
|
self,
|
520
521
|
df: 'pd.DataFrame',
|
522
|
+
safe_copy: bool = True,
|
523
|
+
date_bound_only: bool = False,
|
521
524
|
chunksize: Optional[int] = -1,
|
522
525
|
debug: bool = False,
|
523
526
|
**kw
|
@@ -530,6 +533,14 @@ def filter_existing(
|
|
530
533
|
df: 'pd.DataFrame'
|
531
534
|
The dataframe to inspect and filter.
|
532
535
|
|
536
|
+
safe_copy: bool, default True
|
537
|
+
If `True`, create a copy before comparing and modifying the dataframes.
|
538
|
+
Setting to `False` may mutate the DataFrames.
|
539
|
+
See `meerschaum.utils.dataframe.filter_unseen_df`.
|
540
|
+
|
541
|
+
date_bound_only: bool, default False
|
542
|
+
If `True`, only use the datetime index to fetch the sample dataframe.
|
543
|
+
|
533
544
|
chunksize: Optional[int], default -1
|
534
545
|
The `chunksize` used when fetching existing data.
|
535
546
|
|
@@ -567,7 +578,8 @@ def filter_existing(
|
|
567
578
|
else:
|
568
579
|
merge = pd.merge
|
569
580
|
NA = pd.NA
|
570
|
-
|
581
|
+
if df is None:
|
582
|
+
return df, df, df
|
571
583
|
if (df.empty if not is_dask else len(df) == 0):
|
572
584
|
return df, df, df
|
573
585
|
|
@@ -617,7 +629,7 @@ def filter_existing(
|
|
617
629
|
traceback.print_exc()
|
618
630
|
max_dt = None
|
619
631
|
|
620
|
-
if
|
632
|
+
if ('datetime' not in str(type(max_dt))) or str(min_dt) == 'NaT':
|
621
633
|
if 'int' not in str(type(max_dt)).lower():
|
622
634
|
max_dt = None
|
623
635
|
|
@@ -645,7 +657,7 @@ def filter_existing(
|
|
645
657
|
col: df[col].unique()
|
646
658
|
for col in self.columns
|
647
659
|
if col in df.columns and col != dt_col
|
648
|
-
}
|
660
|
+
} if not date_bound_only else {}
|
649
661
|
filter_params_index_limit = get_config('pipes', 'sync', 'filter_params_index_limit')
|
650
662
|
_ = kw.pop('params', None)
|
651
663
|
params = {
|
@@ -655,7 +667,7 @@ def filter_existing(
|
|
655
667
|
]
|
656
668
|
for col, unique_vals in unique_index_vals.items()
|
657
669
|
if len(unique_vals) <= filter_params_index_limit
|
658
|
-
}
|
670
|
+
} if not date_bound_only else {}
|
659
671
|
|
660
672
|
if debug:
|
661
673
|
dprint(f"Looking at data between '{begin}' and '{end}':", **kw)
|
@@ -698,18 +710,23 @@ def filter_existing(
|
|
698
710
|
col: to_pandas_dtype(typ)
|
699
711
|
for col, typ in self_dtypes.items()
|
700
712
|
},
|
713
|
+
safe_copy = safe_copy,
|
701
714
|
debug = debug
|
702
715
|
),
|
703
716
|
on_cols_dtypes,
|
704
717
|
)
|
705
718
|
|
706
719
|
### Cast dicts or lists to strings so we can merge.
|
720
|
+
serializer = functools.partial(json.dumps, sort_keys=True, separators=(',', ':'), default=str)
|
721
|
+
def deserializer(x):
|
722
|
+
return json.loads(x) if isinstance(x, str) else x
|
723
|
+
|
707
724
|
unhashable_delta_cols = get_unhashable_cols(delta_df)
|
708
725
|
unhashable_backtrack_cols = get_unhashable_cols(backtrack_df)
|
709
726
|
for col in unhashable_delta_cols:
|
710
|
-
delta_df[col] = delta_df[col].apply(
|
727
|
+
delta_df[col] = delta_df[col].apply(serializer)
|
711
728
|
for col in unhashable_backtrack_cols:
|
712
|
-
backtrack_df[col] = backtrack_df[col].apply(
|
729
|
+
backtrack_df[col] = backtrack_df[col].apply(serializer)
|
713
730
|
casted_cols = set(unhashable_delta_cols + unhashable_backtrack_cols)
|
714
731
|
|
715
732
|
joined_df = merge(
|
@@ -722,13 +739,9 @@ def filter_existing(
|
|
722
739
|
) if on_cols else delta_df
|
723
740
|
for col in casted_cols:
|
724
741
|
if col in joined_df.columns:
|
725
|
-
joined_df[col] = joined_df[col].apply(
|
726
|
-
|
727
|
-
|
728
|
-
if isinstance(x, str)
|
729
|
-
else x
|
730
|
-
)
|
731
|
-
)
|
742
|
+
joined_df[col] = joined_df[col].apply(deserializer)
|
743
|
+
if col in delta_df.columns:
|
744
|
+
delta_df[col] = delta_df[col].apply(deserializer)
|
732
745
|
|
733
746
|
### Determine which rows are completely new.
|
734
747
|
new_rows_mask = (joined_df['_merge'] == 'left_only') if on_cols else None
|
meerschaum/core/User/_User.py
CHANGED
@@ -7,22 +7,157 @@ User class definition
|
|
7
7
|
"""
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
10
|
+
import os
|
11
|
+
import hashlib
|
12
|
+
import hmac
|
13
|
+
from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError
|
14
|
+
from meerschaum.utils.typing import Optional, Dict, Any, Tuple
|
15
|
+
from meerschaum.config.static import STATIC_CONFIG
|
16
|
+
from meerschaum.utils.warnings import warn
|
17
|
+
|
18
|
+
|
19
|
+
__all__ = ('hash_password', 'verify_password', 'User')
|
20
|
+
|
21
|
+
def hash_password(
|
22
|
+
password: str,
|
23
|
+
salt: Optional[bytes] = None,
|
24
|
+
rounds: Optional[int] = None,
|
25
|
+
) -> str:
|
26
|
+
"""
|
27
|
+
Return an encoded hash string from the given password.
|
28
|
+
|
29
|
+
Parameters
|
30
|
+
----------
|
31
|
+
password: str
|
32
|
+
The password to be hashed.
|
33
|
+
|
34
|
+
salt: Optional[str], default None
|
35
|
+
If provided, use these bytes for the salt in the hash.
|
36
|
+
Otherwise defaults to 16 random bytes.
|
37
|
+
|
38
|
+
rounds: Optional[int], default None
|
39
|
+
If provided, use this number of rounds to generate the hash.
|
40
|
+
Defaults to 3,000,000.
|
41
|
+
|
42
|
+
Returns
|
43
|
+
-------
|
44
|
+
An encoded hash string to be stored in a database.
|
45
|
+
See the `passlib` documentation on the string format:
|
46
|
+
https://passlib.readthedocs.io/en/stable/lib/passlib.hash.pbkdf2_digest.html#format-algorithm
|
47
|
+
"""
|
48
|
+
hash_config = STATIC_CONFIG['users']['password_hash']
|
49
|
+
if password is None:
|
50
|
+
password = ''
|
51
|
+
if salt is None:
|
52
|
+
salt = os.urandom(hash_config['salt_bytes'])
|
53
|
+
if rounds is None:
|
54
|
+
rounds = hash_config['pbkdf2_sha256__default_rounds']
|
55
|
+
|
56
|
+
pw_hash = hashlib.pbkdf2_hmac(
|
57
|
+
hash_config['algorithm_name'],
|
58
|
+
password.encode('utf-8'),
|
59
|
+
salt,
|
60
|
+
rounds,
|
61
|
+
)
|
62
|
+
return (
|
63
|
+
f"$pbkdf2-{hash_config['algorithm_name']}"
|
64
|
+
+ f"${hash_config['pbkdf2_sha256__default_rounds']}"
|
65
|
+
+ '$' + ab64_encode(salt).decode('utf-8')
|
66
|
+
+ '$' + ab64_encode(pw_hash).decode('utf-8')
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
def verify_password(
|
71
|
+
password: str,
|
72
|
+
password_hash: str,
|
73
|
+
) -> bool:
|
74
|
+
"""
|
75
|
+
Return `True` if the password matches the provided hash.
|
76
|
+
|
77
|
+
Parameters
|
78
|
+
----------
|
79
|
+
password: str
|
80
|
+
The password to be checked.
|
81
|
+
|
82
|
+
password_hash: str
|
83
|
+
The encoded hash string as generated from `hash_password()`.
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
A `bool` indicating whether `password` matches `password_hash`.
|
88
|
+
"""
|
89
|
+
if password is None or password_hash is None:
|
90
|
+
return False
|
91
|
+
hash_config = STATIC_CONFIG['users']['password_hash']
|
92
|
+
try:
|
93
|
+
digest, rounds_str, encoded_salt, encoded_checksum = password_hash.split('$')[1:]
|
94
|
+
algorithm_name = digest.split('-')[-1]
|
95
|
+
salt = ab64_decode(encoded_salt)
|
96
|
+
checksum = ab64_decode(encoded_checksum)
|
97
|
+
rounds = int(rounds_str)
|
98
|
+
except Exception as e:
|
99
|
+
warn(f"Failed to extract context from password hash '{password_hash}'. Is it corrupted?")
|
100
|
+
return False
|
101
|
+
|
102
|
+
return hmac.compare_digest(
|
103
|
+
checksum,
|
104
|
+
hashlib.pbkdf2_hmac(
|
105
|
+
algorithm_name,
|
106
|
+
password.encode('utf-8'),
|
107
|
+
salt,
|
108
|
+
rounds,
|
24
109
|
)
|
25
|
-
|
110
|
+
)
|
111
|
+
|
112
|
+
_BASE64_STRIP = b"=\n"
|
113
|
+
_BASE64_PAD1 = b"="
|
114
|
+
_BASE64_PAD2 = b"=="
|
115
|
+
|
116
|
+
def ab64_encode(data):
|
117
|
+
return b64s_encode(data).replace(b"+", b".")
|
118
|
+
|
119
|
+
def ab64_decode(data):
|
120
|
+
"""
|
121
|
+
decode from shortened base64 format which omits padding & whitespace.
|
122
|
+
uses custom ``./`` altchars, but supports decoding normal ``+/`` altchars as well.
|
123
|
+
"""
|
124
|
+
if isinstance(data, str):
|
125
|
+
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
|
126
|
+
try:
|
127
|
+
data = data.encode("ascii")
|
128
|
+
except UnicodeEncodeError:
|
129
|
+
raise ValueError("string argument should contain only ASCII characters")
|
130
|
+
return b64s_decode(data.replace(b".", b"+"))
|
131
|
+
|
132
|
+
|
133
|
+
def b64s_encode(data):
|
134
|
+
return b2a_base64(data).rstrip(_BASE64_STRIP)
|
135
|
+
|
136
|
+
def b64s_decode(data):
|
137
|
+
"""
|
138
|
+
decode from shortened base64 format which omits padding & whitespace.
|
139
|
+
uses default ``+/`` altchars.
|
140
|
+
"""
|
141
|
+
if isinstance(data, str):
|
142
|
+
# needs bytes for replace() call, but want to accept ascii-unicode ala a2b_base64()
|
143
|
+
try:
|
144
|
+
data = data.encode("ascii")
|
145
|
+
except UnicodeEncodeError as ue:
|
146
|
+
raise ValueError("string argument should contain only ASCII characters") from ue
|
147
|
+
off = len(data) & 3
|
148
|
+
if off == 0:
|
149
|
+
pass
|
150
|
+
elif off == 2:
|
151
|
+
data += _BASE64_PAD2
|
152
|
+
elif off == 3:
|
153
|
+
data += _BASE64_PAD1
|
154
|
+
else: # off == 1
|
155
|
+
raise ValueError("Invalid base64 input")
|
156
|
+
try:
|
157
|
+
return a2b_base64(data)
|
158
|
+
except _BinAsciiError as err:
|
159
|
+
raise TypeError(err) from err
|
160
|
+
|
26
161
|
|
27
162
|
class User:
|
28
163
|
"""
|
@@ -42,7 +177,6 @@ class User:
|
|
42
177
|
if password is None:
|
43
178
|
password = ''
|
44
179
|
self.password = password
|
45
|
-
self.password_hash = get_pwd_context().hash(password)
|
46
180
|
self.username = username
|
47
181
|
self.email = email
|
48
182
|
self.type = type
|
@@ -80,3 +214,11 @@ class User:
|
|
80
214
|
@user_id.setter
|
81
215
|
def user_id(self, user_id):
|
82
216
|
self._user_id = user_id
|
217
|
+
|
218
|
+
@property
|
219
|
+
def password_hash(self):
|
220
|
+
_password_hash = self.__dict__.get('_password_hash', None)
|
221
|
+
if _password_hash is not None:
|
222
|
+
return _password_hash
|
223
|
+
self._password_hash = hash_password(self.password)
|
224
|
+
return self._password_hash
|
meerschaum/core/User/__init__.py
CHANGED
meerschaum/plugins/_Plugin.py
CHANGED
@@ -209,7 +209,7 @@ class Plugin:
|
|
209
209
|
def parse_gitignore() -> 'Set[str]':
|
210
210
|
gitignore_path = pathlib.Path(path) / '.gitignore'
|
211
211
|
if not gitignore_path.exists():
|
212
|
-
return set()
|
212
|
+
return set(default_patterns_to_ignore)
|
213
213
|
with open(gitignore_path, 'r', encoding='utf-8') as f:
|
214
214
|
gitignore_text = f.read()
|
215
215
|
return set(pathspec.PathSpec.from_lines(
|
@@ -252,6 +252,7 @@ class Plugin:
|
|
252
252
|
|
253
253
|
def install(
|
254
254
|
self,
|
255
|
+
skip_deps: bool = False,
|
255
256
|
force: bool = False,
|
256
257
|
debug: bool = False,
|
257
258
|
) -> SuccessTuple:
|
@@ -263,6 +264,9 @@ class Plugin:
|
|
263
264
|
|
264
265
|
Parameters
|
265
266
|
----------
|
267
|
+
skip_deps: bool, default False
|
268
|
+
If `True`, do not install dependencies.
|
269
|
+
|
266
270
|
force: bool, default False
|
267
271
|
If `True`, continue with installation, even if required packages fail to install.
|
268
272
|
|
@@ -366,7 +370,11 @@ class Plugin:
|
|
366
370
|
plugin_installation_dir_path = path
|
367
371
|
break
|
368
372
|
|
369
|
-
success_msg =
|
373
|
+
success_msg = (
|
374
|
+
f"Successfully installed plugin '{self}'"
|
375
|
+
+ ("\n (skipped dependencies)" if skip_deps else "")
|
376
|
+
+ "."
|
377
|
+
)
|
370
378
|
success, abort = None, None
|
371
379
|
|
372
380
|
if is_same_version and not force:
|
@@ -423,7 +431,8 @@ class Plugin:
|
|
423
431
|
return success, msg
|
424
432
|
|
425
433
|
### attempt to install dependencies
|
426
|
-
|
434
|
+
dependencies_installed = skip_deps or self.install_dependencies(force=force, debug=debug)
|
435
|
+
if not dependencies_installed:
|
427
436
|
_ongoing_installations.remove(self.full_name)
|
428
437
|
return False, f"Failed to install dependencies for plugin '{self}'."
|
429
438
|
|
meerschaum/plugins/__init__.py
CHANGED
@@ -247,6 +247,26 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
|
|
247
247
|
_warn(f"Unable to create lockfile {PLUGINS_INTERNAL_LOCK_PATH}:\n{e}")
|
248
248
|
|
249
249
|
with _locks['internal_plugins']:
|
250
|
+
|
251
|
+
try:
|
252
|
+
from importlib.metadata import entry_points
|
253
|
+
except ImportError:
|
254
|
+
importlib_metadata = attempt_import('importlib_metadata', lazy=False)
|
255
|
+
entry_points = importlib_metadata.entry_points
|
256
|
+
|
257
|
+
### NOTE: Allow plugins to be installed via `pip`.
|
258
|
+
packaged_plugin_paths = []
|
259
|
+
discovered_packaged_plugins_eps = entry_points(group='meerschaum.plugins')
|
260
|
+
for ep in discovered_packaged_plugins_eps:
|
261
|
+
module_name = ep.name
|
262
|
+
for package_file_path in ep.dist.files:
|
263
|
+
if package_file_path.suffix != '.py':
|
264
|
+
continue
|
265
|
+
if str(package_file_path) == f'{module_name}.py':
|
266
|
+
packaged_plugin_paths.append(package_file_path.locate())
|
267
|
+
elif str(package_file_path) == f'{module_name}/__init__.py':
|
268
|
+
packaged_plugin_paths.append(package_file_path.locate().parent)
|
269
|
+
|
250
270
|
if is_symlink(PLUGINS_RESOURCES_PATH) or not PLUGINS_RESOURCES_PATH.exists():
|
251
271
|
try:
|
252
272
|
PLUGINS_RESOURCES_PATH.unlink()
|
@@ -255,7 +275,6 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
|
|
255
275
|
|
256
276
|
PLUGINS_RESOURCES_PATH.mkdir(exist_ok=True)
|
257
277
|
|
258
|
-
|
259
278
|
existing_symlinked_paths = [
|
260
279
|
(PLUGINS_RESOURCES_PATH / item)
|
261
280
|
for item in os.listdir(PLUGINS_RESOURCES_PATH)
|
@@ -275,6 +294,7 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
|
|
275
294
|
for plugins_path in PLUGINS_DIR_PATHS
|
276
295
|
]
|
277
296
|
))
|
297
|
+
plugins_to_be_symlinked.extend(packaged_plugin_paths)
|
278
298
|
|
279
299
|
### Check for duplicates.
|
280
300
|
seen_plugins = defaultdict(lambda: 0)
|
@@ -538,6 +558,8 @@ def get_plugins(*to_load, try_import: bool = True) -> Union[Tuple[Plugin], Plugi
|
|
538
558
|
]
|
539
559
|
plugins = tuple(plugin for plugin in _plugins if plugin.is_installed(try_import=try_import))
|
540
560
|
if len(to_load) == 1:
|
561
|
+
if len(plugins) == 0:
|
562
|
+
raise ValueError(f"Plugin '{to_load[0]}' is not installed.")
|
541
563
|
return plugins[0]
|
542
564
|
return plugins
|
543
565
|
|
@@ -15,9 +15,11 @@ import signal
|
|
15
15
|
import sys
|
16
16
|
import time
|
17
17
|
import traceback
|
18
|
+
from functools import partial
|
18
19
|
from datetime import datetime, timezone
|
19
20
|
from meerschaum.utils.typing import Optional, Dict, Any, SuccessTuple, Callable, List, Union
|
20
21
|
from meerschaum.config import get_config
|
22
|
+
from meerschaum.config.static import STATIC_CONFIG
|
21
23
|
from meerschaum.config._paths import DAEMON_RESOURCES_PATH, LOGS_RESOURCES_PATH
|
22
24
|
from meerschaum.config._patch import apply_patch_to_config
|
23
25
|
from meerschaum.utils.warnings import warn, error
|
@@ -36,7 +38,7 @@ class Daemon:
|
|
36
38
|
def __new__(
|
37
39
|
cls,
|
38
40
|
*args,
|
39
|
-
daemon_id
|
41
|
+
daemon_id: Optional[str] = None,
|
40
42
|
**kw
|
41
43
|
):
|
42
44
|
"""
|
@@ -129,7 +131,7 @@ class Daemon:
|
|
129
131
|
keep_daemon_output: bool, default True
|
130
132
|
If `False`, delete the daemon's output directory upon exiting.
|
131
133
|
|
132
|
-
allow_dirty_run :
|
134
|
+
allow_dirty_run, bool, default False:
|
133
135
|
If `True`, run the daemon, even if the `daemon_id` directory exists.
|
134
136
|
This option is dangerous because if the same `daemon_id` runs twice,
|
135
137
|
the last to finish will overwrite the output of the first.
|
@@ -138,8 +140,13 @@ class Daemon:
|
|
138
140
|
-------
|
139
141
|
Nothing — this will exit the parent process.
|
140
142
|
"""
|
141
|
-
import platform, sys, os
|
143
|
+
import platform, sys, os, traceback
|
144
|
+
from meerschaum.config._paths import DAEMON_ERROR_LOG_PATH
|
145
|
+
from meerschaum.utils.warnings import warn
|
146
|
+
from meerschaum.config import get_config
|
142
147
|
daemon = attempt_import('daemon')
|
148
|
+
lines = get_config('jobs', 'terminal', 'lines')
|
149
|
+
columns = get_config('jobs','terminal', 'columns')
|
143
150
|
|
144
151
|
if platform.system() == 'Windows':
|
145
152
|
return False, "Windows is no longer supported."
|
@@ -160,31 +167,45 @@ class Daemon:
|
|
160
167
|
)
|
161
168
|
|
162
169
|
log_refresh_seconds = get_config('jobs', 'logs', 'refresh_files_seconds')
|
163
|
-
self._log_refresh_timer = RepeatTimer(
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
with open(self.pid_path, 'w+') as f:
|
168
|
-
f.write(str(os.getpid()))
|
170
|
+
self._log_refresh_timer = RepeatTimer(
|
171
|
+
log_refresh_seconds,
|
172
|
+
partial(self.rotating_log.refresh_files, start_interception=True),
|
173
|
+
)
|
169
174
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
self.rotating_log.close()
|
179
|
-
if self.pid is None and self.pid_path.exists():
|
180
|
-
self.pid_path.unlink()
|
175
|
+
try:
|
176
|
+
os.environ['LINES'], os.environ['COLUMNS'] = str(int(lines)), str(int(columns))
|
177
|
+
with self._daemon_context:
|
178
|
+
os.environ[STATIC_CONFIG['environment']['daemon_id']] = self.daemon_id
|
179
|
+
self.rotating_log.refresh_files(start_interception=True)
|
180
|
+
try:
|
181
|
+
with open(self.pid_path, 'w+', encoding='utf-8') as f:
|
182
|
+
f.write(str(os.getpid()))
|
181
183
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
184
|
+
self._log_refresh_timer.start()
|
185
|
+
result = self.target(*self.target_args, **self.target_kw)
|
186
|
+
self.properties['result'] = result
|
187
|
+
except Exception as e:
|
188
|
+
warn(e, stacklevel=3)
|
189
|
+
result = e
|
190
|
+
finally:
|
191
|
+
self._log_refresh_timer.cancel()
|
192
|
+
self.rotating_log.close()
|
193
|
+
if self.pid is None and self.pid_path.exists():
|
194
|
+
self.pid_path.unlink()
|
195
|
+
|
196
|
+
if keep_daemon_output:
|
197
|
+
self._capture_process_timestamp('ended')
|
198
|
+
else:
|
199
|
+
self.cleanup()
|
200
|
+
|
201
|
+
return result
|
202
|
+
except Exception as e:
|
203
|
+
daemon_error = traceback.format_exc()
|
204
|
+
with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
|
205
|
+
f.write(daemon_error)
|
186
206
|
|
187
|
-
|
207
|
+
if daemon_error:
|
208
|
+
warn(f"Encountered an error while starting the daemon '{self}':\n{daemon_error}")
|
188
209
|
|
189
210
|
|
190
211
|
def _capture_process_timestamp(
|
@@ -444,6 +465,9 @@ class Daemon:
|
|
444
465
|
Handle `SIGINT` within the Daemon context.
|
445
466
|
This method is injected into the `DaemonContext`.
|
446
467
|
"""
|
468
|
+
# from meerschaum.utils.daemon.FileDescriptorInterceptor import STOP_READING_FD_EVENT
|
469
|
+
# STOP_READING_FD_EVENT.set()
|
470
|
+
self.rotating_log.stop_log_fd_interception(unused_only=False)
|
447
471
|
timer = self.__dict__.get('_log_refresh_timer', None)
|
448
472
|
if timer is not None:
|
449
473
|
timer.cancel()
|
@@ -453,9 +477,17 @@ class Daemon:
|
|
453
477
|
daemon_context.close()
|
454
478
|
|
455
479
|
_close_pools()
|
456
|
-
|
457
|
-
|
458
|
-
|
480
|
+
import threading
|
481
|
+
for thread in threading.enumerate():
|
482
|
+
if thread.name == 'MainThread':
|
483
|
+
continue
|
484
|
+
try:
|
485
|
+
if thread.is_alive():
|
486
|
+
stack = traceback.format_stack(sys._current_frames()[thread.ident])
|
487
|
+
thread.join()
|
488
|
+
except Exception as e:
|
489
|
+
warn(traceback.format_exc())
|
490
|
+
raise KeyboardInterrupt()
|
459
491
|
|
460
492
|
|
461
493
|
def _handle_sigterm(self, signal_number: int, stack_frame: 'frame') -> None:
|
@@ -472,8 +504,7 @@ class Daemon:
|
|
472
504
|
daemon_context.close()
|
473
505
|
|
474
506
|
_close_pools()
|
475
|
-
|
476
|
-
raise SystemExit()
|
507
|
+
raise SystemExit(0)
|
477
508
|
|
478
509
|
|
479
510
|
def _send_signal(
|
@@ -650,7 +681,12 @@ class Daemon:
|
|
650
681
|
if '_rotating_log' in self.__dict__:
|
651
682
|
return self._rotating_log
|
652
683
|
|
653
|
-
self._rotating_log = RotatingFile(
|
684
|
+
self._rotating_log = RotatingFile(
|
685
|
+
self.log_path,
|
686
|
+
redirect_streams = True,
|
687
|
+
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled'),
|
688
|
+
timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format'),
|
689
|
+
)
|
654
690
|
return self._rotating_log
|
655
691
|
|
656
692
|
|
@@ -663,6 +699,8 @@ class Daemon:
|
|
663
699
|
self.rotating_log.file_path,
|
664
700
|
num_files_to_keep = self.rotating_log.num_files_to_keep,
|
665
701
|
max_file_size = self.rotating_log.max_file_size,
|
702
|
+
write_timestamps = get_config('jobs', 'logs', 'timestamps', 'enabled'),
|
703
|
+
timestamp_format = get_config('jobs', 'logs', 'timestamps', 'format'),
|
666
704
|
)
|
667
705
|
return new_rotating_log.read()
|
668
706
|
|
@@ -714,7 +752,7 @@ class Daemon:
|
|
714
752
|
if not self.pid_path.exists():
|
715
753
|
return None
|
716
754
|
try:
|
717
|
-
with open(self.pid_path, 'r') as f:
|
755
|
+
with open(self.pid_path, 'r', encoding='utf-8') as f:
|
718
756
|
text = f.read()
|
719
757
|
pid = int(text.rstrip())
|
720
758
|
except Exception as e:
|
@@ -815,7 +853,7 @@ class Daemon:
|
|
815
853
|
if self.properties is not None:
|
816
854
|
try:
|
817
855
|
self.path.mkdir(parents=True, exist_ok=True)
|
818
|
-
with open(self.properties_path, 'w+') as properties_file:
|
856
|
+
with open(self.properties_path, 'w+', encoding='utf-8') as properties_file:
|
819
857
|
json.dump(self.properties, properties_file)
|
820
858
|
success, msg = True, 'Success'
|
821
859
|
except Exception as e:
|
@@ -865,21 +903,36 @@ class Daemon:
|
|
865
903
|
error(_write_pickle_success_tuple[1])
|
866
904
|
|
867
905
|
|
868
|
-
def cleanup(self, keep_logs: bool = False) ->
|
869
|
-
"""
|
906
|
+
def cleanup(self, keep_logs: bool = False) -> SuccessTuple:
|
907
|
+
"""
|
908
|
+
Remove a daemon's directory after execution.
|
870
909
|
|
871
910
|
Parameters
|
872
911
|
----------
|
873
912
|
keep_logs: bool, default False
|
874
913
|
If `True`, skip deleting the daemon's log files.
|
914
|
+
|
915
|
+
Returns
|
916
|
+
-------
|
917
|
+
A `SuccessTuple` indicating success.
|
875
918
|
"""
|
876
919
|
if self.path.exists():
|
877
920
|
try:
|
878
921
|
shutil.rmtree(self.path)
|
879
922
|
except Exception as e:
|
880
|
-
|
923
|
+
msg = f"Failed to clean up '{self.daemon_id}':\n{e}"
|
924
|
+
warn(msg)
|
925
|
+
return False, msg
|
881
926
|
if not keep_logs:
|
882
927
|
self.rotating_log.delete()
|
928
|
+
try:
|
929
|
+
if self.log_offset_path.exists():
|
930
|
+
self.log_offset_path.unlink()
|
931
|
+
except Exception as e:
|
932
|
+
msg = f"Failed to remove offset file for '{self.daemon_id}':\n{e}"
|
933
|
+
warn(msg)
|
934
|
+
return False, msg
|
935
|
+
return True, "Success"
|
883
936
|
|
884
937
|
|
885
938
|
def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]:
|