meerschaum 2.3.5.dev0__py3-none-any.whl → 2.4.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/_internal/arguments/__init__.py +2 -1
- meerschaum/_internal/arguments/_parse_arguments.py +88 -12
- meerschaum/_internal/docs/index.py +3 -2
- meerschaum/_internal/entry.py +42 -20
- meerschaum/_internal/shell/Shell.py +38 -44
- meerschaum/_internal/term/TermPageHandler.py +2 -3
- meerschaum/_internal/term/__init__.py +13 -11
- meerschaum/actions/api.py +26 -23
- meerschaum/actions/bootstrap.py +38 -11
- meerschaum/actions/copy.py +3 -3
- meerschaum/actions/delete.py +4 -1
- meerschaum/actions/register.py +1 -3
- meerschaum/actions/stack.py +24 -19
- meerschaum/actions/start.py +41 -41
- meerschaum/actions/sync.py +53 -52
- meerschaum/api/__init__.py +48 -14
- meerschaum/api/_events.py +26 -17
- meerschaum/api/_oauth2.py +2 -2
- meerschaum/api/_websockets.py +5 -4
- meerschaum/api/dash/__init__.py +7 -16
- meerschaum/api/dash/callbacks/__init__.py +1 -0
- meerschaum/api/dash/callbacks/dashboard.py +52 -58
- meerschaum/api/dash/callbacks/jobs.py +15 -16
- meerschaum/api/dash/callbacks/login.py +16 -10
- meerschaum/api/dash/callbacks/pipes.py +41 -0
- meerschaum/api/dash/callbacks/plugins.py +1 -1
- meerschaum/api/dash/callbacks/register.py +15 -11
- meerschaum/api/dash/components.py +54 -59
- meerschaum/api/dash/jobs.py +5 -9
- meerschaum/api/dash/pages/__init__.py +1 -0
- meerschaum/api/dash/pages/pipes.py +19 -0
- meerschaum/api/dash/pipes.py +86 -58
- meerschaum/api/dash/plugins.py +6 -4
- meerschaum/api/dash/sessions.py +176 -0
- meerschaum/api/dash/users.py +3 -41
- meerschaum/api/dash/webterm.py +12 -17
- meerschaum/api/resources/static/js/terminado.js +1 -1
- meerschaum/api/routes/_actions.py +4 -118
- meerschaum/api/routes/_jobs.py +45 -24
- meerschaum/api/routes/_login.py +4 -4
- meerschaum/api/routes/_pipes.py +3 -3
- meerschaum/api/routes/_webterm.py +5 -6
- meerschaum/config/_default.py +15 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +64 -21
- meerschaum/config/static/__init__.py +6 -0
- meerschaum/connectors/{Connector.py → _Connector.py} +19 -13
- meerschaum/connectors/__init__.py +24 -14
- meerschaum/connectors/api/{APIConnector.py → _APIConnector.py} +3 -1
- meerschaum/connectors/api/__init__.py +2 -1
- meerschaum/connectors/api/_actions.py +22 -36
- meerschaum/connectors/api/_jobs.py +1 -0
- meerschaum/connectors/parse.py +18 -16
- meerschaum/connectors/poll.py +30 -24
- meerschaum/connectors/sql/__init__.py +3 -1
- meerschaum/connectors/sql/_pipes.py +172 -197
- meerschaum/connectors/sql/_plugins.py +45 -43
- meerschaum/connectors/sql/_users.py +46 -38
- meerschaum/connectors/valkey/_ValkeyConnector.py +535 -0
- meerschaum/connectors/valkey/__init__.py +10 -0
- meerschaum/connectors/valkey/_fetch.py +75 -0
- meerschaum/connectors/valkey/_pipes.py +844 -0
- meerschaum/connectors/valkey/_plugins.py +265 -0
- meerschaum/connectors/valkey/_users.py +305 -0
- meerschaum/core/Pipe/__init__.py +3 -0
- meerschaum/core/Pipe/_attributes.py +1 -2
- meerschaum/core/Pipe/_clear.py +16 -13
- meerschaum/core/Pipe/_copy.py +106 -0
- meerschaum/core/Pipe/_data.py +165 -101
- meerschaum/core/Pipe/_drop.py +4 -4
- meerschaum/core/Pipe/_dtypes.py +14 -14
- meerschaum/core/Pipe/_edit.py +15 -14
- meerschaum/core/Pipe/_sync.py +134 -53
- meerschaum/core/Pipe/_verify.py +11 -11
- meerschaum/core/User/_User.py +14 -12
- meerschaum/jobs/_Job.py +27 -14
- meerschaum/jobs/__init__.py +7 -2
- meerschaum/jobs/systemd.py +20 -8
- meerschaum/plugins/_Plugin.py +17 -13
- meerschaum/utils/_get_pipes.py +14 -20
- meerschaum/utils/dataframe.py +291 -101
- meerschaum/utils/dtypes/__init__.py +31 -6
- meerschaum/utils/dtypes/sql.py +4 -4
- meerschaum/utils/formatting/_shell.py +5 -6
- meerschaum/utils/misc.py +3 -3
- meerschaum/utils/packages/__init__.py +14 -9
- meerschaum/utils/packages/_packages.py +2 -0
- meerschaum/utils/prompt.py +1 -1
- meerschaum/utils/schedule.py +1 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/METADATA +7 -1
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/RECORD +98 -89
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/WHEEL +1 -1
- meerschaum/api/dash/actions.py +0 -255
- /meerschaum/connectors/sql/{SQLConnector.py → _SQLConnector.py} +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/LICENSE +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/NOTICE +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dist-info}/zip-safe +0 -0
meerschaum/core/Pipe/_sync.py
CHANGED
@@ -26,8 +26,6 @@ from meerschaum.utils.typing import (
|
|
26
26
|
SuccessTuple,
|
27
27
|
Dict,
|
28
28
|
List,
|
29
|
-
Iterable,
|
30
|
-
Generator,
|
31
29
|
)
|
32
30
|
from meerschaum.utils.warnings import warn, error
|
33
31
|
|
@@ -266,7 +264,6 @@ def sync(
|
|
266
264
|
**kw
|
267
265
|
)
|
268
266
|
)
|
269
|
-
|
270
267
|
except Exception as e:
|
271
268
|
get_console().print_exception(
|
272
269
|
suppress=[
|
@@ -369,6 +366,11 @@ def sync(
|
|
369
366
|
|
370
367
|
### Cast to a dataframe and ensure datatypes are what we expect.
|
371
368
|
df = self.enforce_dtypes(df, chunksize=chunksize, debug=debug)
|
369
|
+
|
370
|
+
### Capture `numeric` and `json` columns.
|
371
|
+
self._persist_new_json_columns(df, debug=debug)
|
372
|
+
self._persist_new_numeric_columns(df, debug=debug)
|
373
|
+
|
372
374
|
if debug:
|
373
375
|
dprint(
|
374
376
|
"DataFrame to sync:\n"
|
@@ -554,14 +556,15 @@ def exists(
|
|
554
556
|
|
555
557
|
|
556
558
|
def filter_existing(
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
559
|
+
self,
|
560
|
+
df: 'pd.DataFrame',
|
561
|
+
safe_copy: bool = True,
|
562
|
+
date_bound_only: bool = False,
|
563
|
+
include_unchanged_columns: bool = False,
|
564
|
+
chunksize: Optional[int] = -1,
|
565
|
+
debug: bool = False,
|
566
|
+
**kw
|
567
|
+
) -> Tuple['pd.DataFrame', 'pd.DataFrame', 'pd.DataFrame']:
|
565
568
|
"""
|
566
569
|
Inspect a dataframe and filter out rows which already exist in the pipe.
|
567
570
|
|
@@ -569,7 +572,7 @@ def filter_existing(
|
|
569
572
|
----------
|
570
573
|
df: 'pd.DataFrame'
|
571
574
|
The dataframe to inspect and filter.
|
572
|
-
|
575
|
+
|
573
576
|
safe_copy: bool, default True
|
574
577
|
If `True`, create a copy before comparing and modifying the dataframes.
|
575
578
|
Setting to `False` may mutate the DataFrames.
|
@@ -578,6 +581,10 @@ def filter_existing(
|
|
578
581
|
date_bound_only: bool, default False
|
579
582
|
If `True`, only use the datetime index to fetch the sample dataframe.
|
580
583
|
|
584
|
+
include_unchanged_columns: bool, default False
|
585
|
+
If `True`, include the backtrack columns which haven't changed in the update dataframe.
|
586
|
+
This is useful if you can't update individual keys.
|
587
|
+
|
581
588
|
chunksize: Optional[int], default -1
|
582
589
|
The `chunksize` used when fetching existing data.
|
583
590
|
|
@@ -605,7 +612,7 @@ def filter_existing(
|
|
605
612
|
from meerschaum.config import get_config
|
606
613
|
pd = import_pandas()
|
607
614
|
pandas = attempt_import('pandas')
|
608
|
-
if
|
615
|
+
if 'dataframe' not in str(type(df)).lower():
|
609
616
|
df = self.enforce_dtypes(df, chunksize=chunksize, debug=debug)
|
610
617
|
is_dask = 'dask' in df.__module__
|
611
618
|
if is_dask:
|
@@ -615,8 +622,21 @@ def filter_existing(
|
|
615
622
|
else:
|
616
623
|
merge = pd.merge
|
617
624
|
NA = pd.NA
|
625
|
+
|
626
|
+
def get_empty_df():
|
627
|
+
empty_df = pd.DataFrame([])
|
628
|
+
dtypes = dict(df.dtypes) if df is not None else {}
|
629
|
+
dtypes.update(self.dtypes)
|
630
|
+
pd_dtypes = {
|
631
|
+
col: to_pandas_dtype(str(typ))
|
632
|
+
for col, typ in dtypes.items()
|
633
|
+
}
|
634
|
+
return add_missing_cols_to_df(empty_df, pd_dtypes)
|
635
|
+
|
618
636
|
if df is None:
|
619
|
-
|
637
|
+
empty_df = get_empty_df()
|
638
|
+
return empty_df, empty_df, empty_df
|
639
|
+
|
620
640
|
if (df.empty if not is_dask else len(df) == 0):
|
621
641
|
return df, df, df
|
622
642
|
|
@@ -633,7 +653,7 @@ def filter_existing(
|
|
633
653
|
if min_dt_val is not None and 'datetime' in str(dt_type)
|
634
654
|
else min_dt_val
|
635
655
|
)
|
636
|
-
except Exception
|
656
|
+
except Exception:
|
637
657
|
min_dt = None
|
638
658
|
if not ('datetime' in str(type(min_dt))) or str(min_dt) == 'NaT':
|
639
659
|
if 'int' not in str(type(min_dt)).lower():
|
@@ -643,7 +663,7 @@ def filter_existing(
|
|
643
663
|
begin = (
|
644
664
|
round_time(
|
645
665
|
min_dt,
|
646
|
-
to
|
666
|
+
to='down'
|
647
667
|
) - timedelta(minutes=1)
|
648
668
|
)
|
649
669
|
elif dt_type and 'int' in dt_type.lower():
|
@@ -661,7 +681,7 @@ def filter_existing(
|
|
661
681
|
if max_dt_val is not None and 'datetime' in str(dt_type)
|
662
682
|
else max_dt_val
|
663
683
|
)
|
664
|
-
except Exception
|
684
|
+
except Exception:
|
665
685
|
import traceback
|
666
686
|
traceback.print_exc()
|
667
687
|
max_dt = None
|
@@ -674,14 +694,14 @@ def filter_existing(
|
|
674
694
|
end = (
|
675
695
|
round_time(
|
676
696
|
max_dt,
|
677
|
-
to
|
697
|
+
to='down'
|
678
698
|
) + timedelta(minutes=1)
|
679
699
|
)
|
680
700
|
elif dt_type and 'int' in dt_type.lower():
|
681
701
|
end = max_dt + 1
|
682
702
|
|
683
703
|
if max_dt is not None and min_dt is not None and min_dt > max_dt:
|
684
|
-
warn(
|
704
|
+
warn("Detected minimum datetime greater than maximum datetime.")
|
685
705
|
|
686
706
|
if begin is not None and end is not None and begin > end:
|
687
707
|
if isinstance(begin, datetime):
|
@@ -710,13 +730,18 @@ def filter_existing(
|
|
710
730
|
dprint(f"Looking at data between '{begin}' and '{end}':", **kw)
|
711
731
|
|
712
732
|
backtrack_df = self.get_data(
|
713
|
-
begin
|
714
|
-
end
|
715
|
-
chunksize
|
716
|
-
params
|
717
|
-
debug
|
733
|
+
begin=begin,
|
734
|
+
end=end,
|
735
|
+
chunksize=chunksize,
|
736
|
+
params=params,
|
737
|
+
debug=debug,
|
718
738
|
**kw
|
719
739
|
)
|
740
|
+
if backtrack_df is None:
|
741
|
+
if debug:
|
742
|
+
dprint(f"No backtrack data was found for {self}.")
|
743
|
+
return df, get_empty_df(), df
|
744
|
+
|
720
745
|
if debug:
|
721
746
|
dprint(f"Existing data for {self}:\n" + str(backtrack_df), **kw)
|
722
747
|
dprint(f"Existing dtypes for {self}:\n" + str(backtrack_df.dtypes))
|
@@ -743,18 +768,19 @@ def filter_existing(
|
|
743
768
|
filter_unseen_df(
|
744
769
|
backtrack_df,
|
745
770
|
df,
|
746
|
-
dtypes
|
771
|
+
dtypes={
|
747
772
|
col: to_pandas_dtype(typ)
|
748
773
|
for col, typ in self_dtypes.items()
|
749
774
|
},
|
750
|
-
safe_copy
|
751
|
-
debug
|
775
|
+
safe_copy=safe_copy,
|
776
|
+
debug=debug
|
752
777
|
),
|
753
778
|
on_cols_dtypes,
|
754
779
|
)
|
755
780
|
|
756
781
|
### Cast dicts or lists to strings so we can merge.
|
757
782
|
serializer = functools.partial(json.dumps, sort_keys=True, separators=(',', ':'), default=str)
|
783
|
+
|
758
784
|
def deserializer(x):
|
759
785
|
return json.loads(x) if isinstance(x, str) else x
|
760
786
|
|
@@ -767,12 +793,12 @@ def filter_existing(
|
|
767
793
|
casted_cols = set(unhashable_delta_cols + unhashable_backtrack_cols)
|
768
794
|
|
769
795
|
joined_df = merge(
|
770
|
-
delta_df.fillna(NA),
|
771
|
-
backtrack_df.fillna(NA),
|
772
|
-
how
|
773
|
-
on
|
774
|
-
indicator
|
775
|
-
suffixes
|
796
|
+
delta_df.infer_objects(copy=False).fillna(NA),
|
797
|
+
backtrack_df.infer_objects(copy=False).fillna(NA),
|
798
|
+
how='left',
|
799
|
+
on=on_cols,
|
800
|
+
indicator=True,
|
801
|
+
suffixes=('', '_old'),
|
776
802
|
) if on_cols else delta_df
|
777
803
|
for col in casted_cols:
|
778
804
|
if col in joined_df.columns:
|
@@ -782,20 +808,13 @@ def filter_existing(
|
|
782
808
|
|
783
809
|
### Determine which rows are completely new.
|
784
810
|
new_rows_mask = (joined_df['_merge'] == 'left_only') if on_cols else None
|
785
|
-
cols = list(
|
811
|
+
cols = list(delta_df.columns)
|
786
812
|
|
787
813
|
unseen_df = (
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
.reset_index(drop=True)
|
793
|
-
) if not is_dask else (
|
794
|
-
joined_df
|
795
|
-
.where(new_rows_mask)
|
796
|
-
.dropna(how='all')[cols]
|
797
|
-
.reset_index(drop=True)
|
798
|
-
)
|
814
|
+
joined_df
|
815
|
+
.where(new_rows_mask)
|
816
|
+
.dropna(how='all')[cols]
|
817
|
+
.reset_index(drop=True)
|
799
818
|
) if on_cols else delta_df
|
800
819
|
|
801
820
|
### Rows that have already been inserted but values have changed.
|
@@ -804,20 +823,33 @@ def filter_existing(
|
|
804
823
|
.where(~new_rows_mask)
|
805
824
|
.dropna(how='all')[cols]
|
806
825
|
.reset_index(drop=True)
|
807
|
-
) if on_cols else
|
826
|
+
) if on_cols else get_empty_df()
|
827
|
+
|
828
|
+
if include_unchanged_columns and on_cols:
|
829
|
+
unchanged_backtrack_cols = [
|
830
|
+
col
|
831
|
+
for col in backtrack_df.columns
|
832
|
+
if col in on_cols or col not in update_df.columns
|
833
|
+
]
|
834
|
+
update_df = merge(
|
835
|
+
backtrack_df[unchanged_backtrack_cols],
|
836
|
+
update_df,
|
837
|
+
how='inner',
|
838
|
+
on=on_cols,
|
839
|
+
)
|
808
840
|
|
809
841
|
return unseen_df, update_df, delta_df
|
810
842
|
|
811
843
|
|
812
844
|
@staticmethod
|
813
845
|
def _get_chunk_label(
|
814
|
-
|
815
|
-
|
816
|
-
|
817
|
-
|
818
|
-
|
819
|
-
|
820
|
-
|
846
|
+
chunk: Union[
|
847
|
+
'pd.DataFrame',
|
848
|
+
List[Dict[str, Any]],
|
849
|
+
Dict[str, List[Any]]
|
850
|
+
],
|
851
|
+
dt_col: str,
|
852
|
+
) -> str:
|
821
853
|
"""
|
822
854
|
Return the min - max label for the chunk.
|
823
855
|
"""
|
@@ -870,3 +902,52 @@ def get_num_workers(self, workers: Optional[int] = None) -> int:
|
|
870
902
|
(desired_workers - current_num_connections),
|
871
903
|
1,
|
872
904
|
)
|
905
|
+
|
906
|
+
|
907
|
+
def _persist_new_numeric_columns(self, df, debug: bool = False) -> SuccessTuple:
|
908
|
+
"""
|
909
|
+
Check for new numeric columns and update the parameters.
|
910
|
+
"""
|
911
|
+
from meerschaum.utils.dataframe import get_numeric_cols
|
912
|
+
numeric_cols = get_numeric_cols(df)
|
913
|
+
existing_numeric_cols = [col for col, typ in self.dtypes.items() if typ == 'numeric']
|
914
|
+
new_numeric_cols = [col for col in numeric_cols if col not in existing_numeric_cols]
|
915
|
+
if not new_numeric_cols:
|
916
|
+
return True, "Success"
|
917
|
+
|
918
|
+
dtypes = self.parameters.get('dtypes', {})
|
919
|
+
dtypes.update({col: 'numeric' for col in numeric_cols})
|
920
|
+
self.parameters['dtypes'] = dtypes
|
921
|
+
if not self.temporary:
|
922
|
+
edit_success, edit_msg = self.edit(interactive=False, debug=debug)
|
923
|
+
if not edit_success:
|
924
|
+
warn(f"Unable to update NUMERIC dtypes for {self}:\n{edit_msg}")
|
925
|
+
|
926
|
+
return edit_success, edit_msg
|
927
|
+
|
928
|
+
return True, "Success"
|
929
|
+
|
930
|
+
|
931
|
+
def _persist_new_json_columns(self, df, debug: bool = False) -> SuccessTuple:
|
932
|
+
"""
|
933
|
+
Check for new JSON columns and update the parameters.
|
934
|
+
"""
|
935
|
+
from meerschaum.utils.dataframe import get_json_cols
|
936
|
+
json_cols = get_json_cols(df)
|
937
|
+
existing_json_cols = [col for col, typ in self.dtypes.items() if typ == 'json']
|
938
|
+
new_json_cols = [col for col in json_cols if col not in existing_json_cols]
|
939
|
+
if not new_json_cols:
|
940
|
+
return True, "Success"
|
941
|
+
|
942
|
+
dtypes = self.parameters.get('dtypes', {})
|
943
|
+
dtypes.update({col: 'json' for col in json_cols})
|
944
|
+
self.parameters['dtypes'] = dtypes
|
945
|
+
|
946
|
+
if not self.temporary:
|
947
|
+
edit_success, edit_msg = self.edit(interactive=False, debug=debug)
|
948
|
+
if not edit_success:
|
949
|
+
warn(f"Unable to update JSON dtypes for {self}:\n{edit_msg}")
|
950
|
+
|
951
|
+
return edit_success, edit_msg
|
952
|
+
|
953
|
+
return True, "Success"
|
meerschaum/core/Pipe/_verify.py
CHANGED
@@ -12,17 +12,17 @@ from meerschaum.utils.warnings import warn, info
|
|
12
12
|
from meerschaum.utils.debug import dprint
|
13
13
|
|
14
14
|
def verify(
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
15
|
+
self,
|
16
|
+
begin: Union[datetime, int, None] = None,
|
17
|
+
end: Union[datetime, int, None] = None,
|
18
|
+
params: Optional[Dict[str, Any]] = None,
|
19
|
+
chunk_interval: Union[timedelta, int, None] = None,
|
20
|
+
bounded: Optional[bool] = None,
|
21
|
+
deduplicate: bool = False,
|
22
|
+
workers: Optional[int] = None,
|
23
|
+
debug: bool = False,
|
24
|
+
**kwargs: Any
|
25
|
+
) -> SuccessTuple:
|
26
26
|
"""
|
27
27
|
Verify the contents of the pipe by resyncing its interval.
|
28
28
|
|
meerschaum/core/User/_User.py
CHANGED
@@ -11,7 +11,7 @@ import os
|
|
11
11
|
import hashlib
|
12
12
|
import hmac
|
13
13
|
from binascii import b2a_base64, a2b_base64, Error as _BinAsciiError
|
14
|
-
from meerschaum.utils.typing import Optional, Dict, Any,
|
14
|
+
from meerschaum.utils.typing import Optional, Dict, Any, Union
|
15
15
|
from meerschaum.config.static import STATIC_CONFIG
|
16
16
|
from meerschaum.utils.warnings import warn
|
17
17
|
|
@@ -19,10 +19,10 @@ from meerschaum.utils.warnings import warn
|
|
19
19
|
__all__ = ('hash_password', 'verify_password', 'User')
|
20
20
|
|
21
21
|
def hash_password(
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
password: str,
|
23
|
+
salt: Optional[bytes] = None,
|
24
|
+
rounds: Optional[int] = None,
|
25
|
+
) -> str:
|
26
26
|
"""
|
27
27
|
Return an encoded hash string from the given password.
|
28
28
|
|
@@ -68,9 +68,9 @@ def hash_password(
|
|
68
68
|
|
69
69
|
|
70
70
|
def verify_password(
|
71
|
-
|
72
|
-
|
73
|
-
|
71
|
+
password: str,
|
72
|
+
password_hash: str,
|
73
|
+
) -> bool:
|
74
74
|
"""
|
75
75
|
Return `True` if the password matches the provided hash.
|
76
76
|
|
@@ -197,26 +197,28 @@ class User:
|
|
197
197
|
return self._attributes
|
198
198
|
|
199
199
|
@property
|
200
|
-
def instance_connector(self) ->
|
201
|
-
""" """
|
200
|
+
def instance_connector(self) -> 'mrsm.connectors.Connector':
|
202
201
|
from meerschaum.connectors.parse import parse_instance_keys
|
203
202
|
if '_instance_connector' not in self.__dict__:
|
204
203
|
self._instance_connector = parse_instance_keys(self._instance_keys)
|
205
204
|
return self._instance_connector
|
206
205
|
|
207
206
|
@property
|
208
|
-
def user_id(self) -> int:
|
207
|
+
def user_id(self) -> Union[int, str, None]:
|
209
208
|
"""NOTE: This causes recursion with the API,
|
210
209
|
so don't try to get fancy with read-only attributes.
|
211
210
|
"""
|
212
211
|
return self._user_id
|
213
212
|
|
214
213
|
@user_id.setter
|
215
|
-
def user_id(self, user_id):
|
214
|
+
def user_id(self, user_id: Union[int, str, None]):
|
216
215
|
self._user_id = user_id
|
217
216
|
|
218
217
|
@property
|
219
218
|
def password_hash(self):
|
219
|
+
"""
|
220
|
+
Return the hash of the user's password.
|
221
|
+
"""
|
220
222
|
_password_hash = self.__dict__.get('_password_hash', None)
|
221
223
|
if _password_hash is not None:
|
222
224
|
return _password_hash
|
meerschaum/jobs/_Job.py
CHANGED
@@ -10,10 +10,7 @@ from __future__ import annotations
|
|
10
10
|
|
11
11
|
import shlex
|
12
12
|
import asyncio
|
13
|
-
import threading
|
14
|
-
import json
|
15
13
|
import pathlib
|
16
|
-
import os
|
17
14
|
import sys
|
18
15
|
import traceback
|
19
16
|
from functools import partial
|
@@ -60,9 +57,10 @@ class Job:
|
|
60
57
|
sysargs: Union[List[str], str, None] = None,
|
61
58
|
env: Optional[Dict[str, str]] = None,
|
62
59
|
executor_keys: Optional[str] = None,
|
60
|
+
delete_after_completion: bool = False,
|
63
61
|
_properties: Optional[Dict[str, Any]] = None,
|
64
|
-
_rotating_log
|
65
|
-
_stdin_file
|
62
|
+
_rotating_log=None,
|
63
|
+
_stdin_file=None,
|
66
64
|
_status_hook: Optional[Callable[[], str]] = None,
|
67
65
|
_result_hook: Optional[Callable[[], SuccessTuple]] = None,
|
68
66
|
_externally_managed: bool = False,
|
@@ -85,6 +83,9 @@ class Job:
|
|
85
83
|
executor_keys: Optional[str], default None
|
86
84
|
If provided, execute the job remotely on an API instance, e.g. 'api:main'.
|
87
85
|
|
86
|
+
delete_after_completion: bool, default False
|
87
|
+
If `True`, delete this job when it has finished executing.
|
88
|
+
|
88
89
|
_properties: Optional[Dict[str, Any]], default None
|
89
90
|
If provided, use this to patch the daemon's properties.
|
90
91
|
"""
|
@@ -146,6 +147,9 @@ class Job:
|
|
146
147
|
if env:
|
147
148
|
self._properties_patch.update({'env': env})
|
148
149
|
|
150
|
+
if delete_after_completion:
|
151
|
+
self._properties_patch.update({'delete_after_completion': delete_after_completion})
|
152
|
+
|
149
153
|
daemon_sysargs = (
|
150
154
|
self._daemon.properties.get('target', {}).get('args', [None])[0]
|
151
155
|
if self._daemon is not None
|
@@ -199,13 +203,11 @@ class Job:
|
|
199
203
|
jobs_dir = root_dir / DAEMON_RESOURCES_PATH.name
|
200
204
|
daemon_dir = jobs_dir / daemon_id
|
201
205
|
pid_file = daemon_dir / 'process.pid'
|
202
|
-
properties_path = daemon_dir / 'properties.json'
|
203
|
-
pickle_path = daemon_dir / 'pickle.pkl'
|
204
206
|
|
205
207
|
if pid_file.exists():
|
206
208
|
with open(pid_file, 'r', encoding='utf-8') as f:
|
207
209
|
daemon_pid = int(f.read())
|
208
|
-
|
210
|
+
|
209
211
|
if pid != daemon_pid:
|
210
212
|
raise EnvironmentError(f"Differing PIDs: {pid=}, {daemon_pid=}")
|
211
213
|
else:
|
@@ -245,7 +247,7 @@ class Job:
|
|
245
247
|
return True, f"{self} is already running."
|
246
248
|
|
247
249
|
success, msg = self.daemon.run(
|
248
|
-
keep_daemon_output=
|
250
|
+
keep_daemon_output=(not self.delete_after_completion),
|
249
251
|
allow_dirty_run=True,
|
250
252
|
)
|
251
253
|
if not success:
|
@@ -407,7 +409,6 @@ class Job:
|
|
407
409
|
)
|
408
410
|
return asyncio.run(monitor_logs_coroutine)
|
409
411
|
|
410
|
-
|
411
412
|
async def monitor_logs_async(
|
412
413
|
self,
|
413
414
|
callback_function: Callable[[str], None] = partial(print, end='', flush=True),
|
@@ -418,8 +419,8 @@ class Job:
|
|
418
419
|
strip_timestamps: bool = False,
|
419
420
|
accept_input: bool = True,
|
420
421
|
_logs_path: Optional[pathlib.Path] = None,
|
421
|
-
_log
|
422
|
-
_stdin_file
|
422
|
+
_log=None,
|
423
|
+
_stdin_file=None,
|
423
424
|
debug: bool = False,
|
424
425
|
):
|
425
426
|
"""
|
@@ -466,6 +467,7 @@ class Job:
|
|
466
467
|
input_callback_function=input_callback_function,
|
467
468
|
stop_callback_function=stop_callback_function,
|
468
469
|
stop_on_exit=stop_on_exit,
|
470
|
+
strip_timestamps=strip_timestamps,
|
469
471
|
accept_input=accept_input,
|
470
472
|
debug=debug,
|
471
473
|
)
|
@@ -557,7 +559,6 @@ class Job:
|
|
557
559
|
for task in pending:
|
558
560
|
task.cancel()
|
559
561
|
except asyncio.exceptions.CancelledError:
|
560
|
-
print('cancelled?')
|
561
562
|
pass
|
562
563
|
finally:
|
563
564
|
combined_event.set()
|
@@ -870,7 +871,9 @@ class Job:
|
|
870
871
|
"""
|
871
872
|
Return the job's Daemon label (joined sysargs).
|
872
873
|
"""
|
873
|
-
|
874
|
+
from meerschaum._internal.arguments import compress_pipeline_sysargs
|
875
|
+
sysargs = compress_pipeline_sysargs(self.sysargs)
|
876
|
+
return shlex.join(sysargs).replace(' + ', '\n+ ')
|
874
877
|
|
875
878
|
@property
|
876
879
|
def _externally_managed_file(self) -> pathlib.Path:
|
@@ -916,6 +919,16 @@ class Job:
|
|
916
919
|
self._env = {**default_env, **_env}
|
917
920
|
return self._env
|
918
921
|
|
922
|
+
@property
|
923
|
+
def delete_after_completion(self) -> bool:
|
924
|
+
"""
|
925
|
+
Return whether this job is configured to delete itself after completion.
|
926
|
+
"""
|
927
|
+
if '_delete_after_completion' in self.__dict__:
|
928
|
+
return self.__dict__.get('_delete_after_completion', False)
|
929
|
+
|
930
|
+
self._delete_after_completion = self.daemon.properties.get('delete_after_completion', False)
|
931
|
+
return self._delete_after_completion
|
919
932
|
|
920
933
|
def __str__(self) -> str:
|
921
934
|
sysargs = self.sysargs
|
meerschaum/jobs/__init__.py
CHANGED
@@ -9,9 +9,9 @@ Higher-level utilities for managing `meerschaum.utils.daemon.Daemon`.
|
|
9
9
|
import pathlib
|
10
10
|
|
11
11
|
import meerschaum as mrsm
|
12
|
-
from meerschaum.utils.typing import Dict, Optional, List,
|
12
|
+
from meerschaum.utils.typing import Dict, Optional, List, SuccessTuple
|
13
13
|
|
14
|
-
from meerschaum.jobs._Job import Job
|
14
|
+
from meerschaum.jobs._Job import Job
|
15
15
|
from meerschaum.jobs._Executor import Executor
|
16
16
|
|
17
17
|
__all__ = (
|
@@ -403,9 +403,14 @@ def get_executor_keys_from_context() -> str:
|
|
403
403
|
if _context_keys is not None:
|
404
404
|
return _context_keys
|
405
405
|
|
406
|
+
from meerschaum.config import get_config
|
406
407
|
from meerschaum.config.paths import ROOT_DIR_PATH, DEFAULT_ROOT_DIR_PATH
|
407
408
|
from meerschaum.utils.misc import is_systemd_available
|
408
409
|
|
410
|
+
configured_executor = get_config('meerschaum', 'executor', warn=False)
|
411
|
+
if configured_executor is not None:
|
412
|
+
return configured_executor
|
413
|
+
|
409
414
|
_context_keys = (
|
410
415
|
'systemd'
|
411
416
|
if is_systemd_available() and ROOT_DIR_PATH == DEFAULT_ROOT_DIR_PATH
|
meerschaum/jobs/systemd.py
CHANGED
@@ -42,7 +42,12 @@ class SystemdExecutor(Executor):
|
|
42
42
|
return [
|
43
43
|
service_name[len('mrsm-'):(-1 * len('.service'))]
|
44
44
|
for service_name in os.listdir(SYSTEMD_USER_RESOURCES_PATH)
|
45
|
-
if
|
45
|
+
if (
|
46
|
+
service_name.startswith('mrsm-')
|
47
|
+
and service_name.endswith('.service')
|
48
|
+
### Check for broken symlinks.
|
49
|
+
and (SYSTEMD_USER_RESOURCES_PATH / service_name).exists()
|
50
|
+
)
|
46
51
|
]
|
47
52
|
|
48
53
|
def get_job_exists(self, name: str, debug: bool = False) -> bool:
|
@@ -146,6 +151,11 @@ class SystemdExecutor(Executor):
|
|
146
151
|
STATIC_CONFIG['environment']['systemd_log_path']: service_logs_path.as_posix(),
|
147
152
|
STATIC_CONFIG['environment']['systemd_result_path']: result_path.as_posix(),
|
148
153
|
STATIC_CONFIG['environment']['systemd_stdin_path']: socket_path.as_posix(),
|
154
|
+
STATIC_CONFIG['environment']['systemd_delete_job']: (
|
155
|
+
'1'
|
156
|
+
if job.delete_after_completion
|
157
|
+
else '0',
|
158
|
+
),
|
149
159
|
})
|
150
160
|
|
151
161
|
### Allow for user-defined environment variables.
|
@@ -603,7 +613,8 @@ class SystemdExecutor(Executor):
|
|
603
613
|
|
604
614
|
check_timeout_interval = get_config('jobs', 'check_timeout_interval_seconds')
|
605
615
|
loop_start = time.perf_counter()
|
606
|
-
|
616
|
+
timeout_seconds = get_config('jobs', 'timeout_seconds')
|
617
|
+
while (time.perf_counter() - loop_start) < timeout_seconds:
|
607
618
|
if self.get_job_status(name, debug=debug) == 'stopped':
|
608
619
|
return True, 'Success'
|
609
620
|
|
@@ -630,12 +641,14 @@ class SystemdExecutor(Executor):
|
|
630
641
|
Delete a job's service.
|
631
642
|
"""
|
632
643
|
from meerschaum.config.paths import SYSTEMD_LOGS_RESOURCES_PATH
|
644
|
+
job = self.get_hidden_job(name, debug=debug)
|
633
645
|
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
646
|
+
if not job.delete_after_completion:
|
647
|
+
_ = self.stop_job(name, debug=debug)
|
648
|
+
_ = self.run_command(
|
649
|
+
['disable', self.get_service_name(name, debug=debug)],
|
650
|
+
debug=debug,
|
651
|
+
)
|
639
652
|
|
640
653
|
service_job_path = self.get_service_job_path(name, debug=debug)
|
641
654
|
try:
|
@@ -666,7 +679,6 @@ class SystemdExecutor(Executor):
|
|
666
679
|
warn(e)
|
667
680
|
return False, str(e)
|
668
681
|
|
669
|
-
job = self.get_hidden_job(name, debug=debug)
|
670
682
|
_ = job.delete()
|
671
683
|
|
672
684
|
return self.run_command(['daemon-reload'], debug=debug)
|