meerschaum 2.3.5.dev0__py3-none-any.whl → 2.4.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. meerschaum/_internal/arguments/__init__.py +2 -1
  2. meerschaum/_internal/arguments/_parse_arguments.py +86 -7
  3. meerschaum/_internal/entry.py +29 -13
  4. meerschaum/actions/api.py +16 -16
  5. meerschaum/actions/bootstrap.py +36 -10
  6. meerschaum/actions/start.py +16 -15
  7. meerschaum/api/_events.py +11 -7
  8. meerschaum/api/dash/__init__.py +7 -6
  9. meerschaum/api/dash/callbacks/__init__.py +1 -0
  10. meerschaum/api/dash/callbacks/dashboard.py +7 -5
  11. meerschaum/api/dash/callbacks/pipes.py +42 -0
  12. meerschaum/api/dash/pages/__init__.py +1 -0
  13. meerschaum/api/dash/pages/pipes.py +16 -0
  14. meerschaum/api/dash/pipes.py +79 -47
  15. meerschaum/api/dash/users.py +19 -6
  16. meerschaum/api/routes/_actions.py +0 -98
  17. meerschaum/api/routes/_jobs.py +38 -18
  18. meerschaum/api/routes/_login.py +4 -4
  19. meerschaum/api/routes/_pipes.py +3 -3
  20. meerschaum/config/_default.py +9 -2
  21. meerschaum/config/_version.py +1 -1
  22. meerschaum/config/stack/__init__.py +59 -18
  23. meerschaum/config/static/__init__.py +2 -0
  24. meerschaum/connectors/Connector.py +19 -13
  25. meerschaum/connectors/__init__.py +9 -5
  26. meerschaum/connectors/api/_actions.py +22 -36
  27. meerschaum/connectors/api/_jobs.py +1 -0
  28. meerschaum/connectors/poll.py +30 -24
  29. meerschaum/connectors/sql/_pipes.py +126 -154
  30. meerschaum/connectors/sql/_plugins.py +45 -43
  31. meerschaum/connectors/sql/_users.py +46 -38
  32. meerschaum/connectors/valkey/ValkeyConnector.py +535 -0
  33. meerschaum/connectors/valkey/__init__.py +8 -0
  34. meerschaum/connectors/valkey/_fetch.py +75 -0
  35. meerschaum/connectors/valkey/_pipes.py +839 -0
  36. meerschaum/connectors/valkey/_plugins.py +265 -0
  37. meerschaum/connectors/valkey/_users.py +305 -0
  38. meerschaum/core/Pipe/__init__.py +2 -0
  39. meerschaum/core/Pipe/_attributes.py +1 -2
  40. meerschaum/core/Pipe/_drop.py +4 -4
  41. meerschaum/core/Pipe/_dtypes.py +14 -14
  42. meerschaum/core/Pipe/_edit.py +15 -14
  43. meerschaum/core/Pipe/_sync.py +134 -51
  44. meerschaum/core/User/_User.py +14 -12
  45. meerschaum/jobs/_Job.py +26 -8
  46. meerschaum/jobs/systemd.py +20 -8
  47. meerschaum/plugins/_Plugin.py +17 -13
  48. meerschaum/utils/_get_pipes.py +14 -20
  49. meerschaum/utils/dataframe.py +288 -101
  50. meerschaum/utils/dtypes/__init__.py +31 -6
  51. meerschaum/utils/dtypes/sql.py +4 -4
  52. meerschaum/utils/misc.py +3 -3
  53. meerschaum/utils/packages/_packages.py +1 -0
  54. meerschaum/utils/prompt.py +1 -1
  55. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/METADATA +3 -1
  56. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/RECORD +62 -54
  57. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/WHEEL +1 -1
  58. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/LICENSE +0 -0
  59. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/NOTICE +0 -0
  60. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/entry_points.txt +0 -0
  61. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/top_level.txt +0 -0
  62. {meerschaum-2.3.5.dev0.dist-info → meerschaum-2.4.0.dev0.dist-info}/zip-safe +0 -0
@@ -18,12 +18,12 @@ def update(self, *args, **kw) -> SuccessTuple:
18
18
 
19
19
 
20
20
  def edit(
21
- self,
22
- patch: bool = False,
23
- interactive: bool = False,
24
- debug: bool = False,
25
- **kw: Any
26
- ) -> SuccessTuple:
21
+ self,
22
+ patch: bool = False,
23
+ interactive: bool = False,
24
+ debug: bool = False,
25
+ **kw: Any
26
+ ) -> SuccessTuple:
27
27
  """
28
28
  Edit a Pipe's configuration.
29
29
 
@@ -50,11 +50,12 @@ def edit(
50
50
  if not interactive:
51
51
  with Venv(get_connector_plugin(self.instance_connector)):
52
52
  return self.instance_connector.edit_pipe(self, patch=patch, debug=debug, **kw)
53
+
53
54
  from meerschaum.config._paths import PIPES_CACHE_RESOURCES_PATH
54
55
  from meerschaum.utils.misc import edit_file
55
56
  parameters_filename = str(self) + '.yaml'
56
57
  parameters_path = PIPES_CACHE_RESOURCES_PATH / parameters_filename
57
-
58
+
58
59
  from meerschaum.utils.yaml import yaml
59
60
 
60
61
  edit_text = f"Edit the parameters for {self}"
@@ -96,13 +97,13 @@ def edit(
96
97
 
97
98
 
98
99
  def edit_definition(
99
- self,
100
- yes: bool = False,
101
- noask: bool = False,
102
- force: bool = False,
103
- debug : bool = False,
104
- **kw : Any
105
- ) -> SuccessTuple:
100
+ self,
101
+ yes: bool = False,
102
+ noask: bool = False,
103
+ force: bool = False,
104
+ debug : bool = False,
105
+ **kw : Any
106
+ ) -> SuccessTuple:
106
107
  """
107
108
  Edit a pipe's definition file and update its configuration.
108
109
  **NOTE:** This function is interactive and should not be used in automated scripts!
@@ -266,7 +266,6 @@ def sync(
266
266
  **kw
267
267
  )
268
268
  )
269
-
270
269
  except Exception as e:
271
270
  get_console().print_exception(
272
271
  suppress=[
@@ -369,6 +368,11 @@ def sync(
369
368
 
370
369
  ### Cast to a dataframe and ensure datatypes are what we expect.
371
370
  df = self.enforce_dtypes(df, chunksize=chunksize, debug=debug)
371
+
372
+ ### Capture `numeric` and `json` columns.
373
+ self._persist_new_json_columns(df, debug=debug)
374
+ self._persist_new_numeric_columns(df, debug=debug)
375
+
372
376
  if debug:
373
377
  dprint(
374
378
  "DataFrame to sync:\n"
@@ -554,14 +558,15 @@ def exists(
554
558
 
555
559
 
556
560
  def filter_existing(
557
- self,
558
- df: 'pd.DataFrame',
559
- safe_copy: bool = True,
560
- date_bound_only: bool = False,
561
- chunksize: Optional[int] = -1,
562
- debug: bool = False,
563
- **kw
564
- ) -> Tuple['pd.DataFrame', 'pd.DataFrame', 'pd.DataFrame']:
561
+ self,
562
+ df: 'pd.DataFrame',
563
+ safe_copy: bool = True,
564
+ date_bound_only: bool = False,
565
+ include_unchanged_columns: bool = False,
566
+ chunksize: Optional[int] = -1,
567
+ debug: bool = False,
568
+ **kw
569
+ ) -> Tuple['pd.DataFrame', 'pd.DataFrame', 'pd.DataFrame']:
565
570
  """
566
571
  Inspect a dataframe and filter out rows which already exist in the pipe.
567
572
 
@@ -569,7 +574,7 @@ def filter_existing(
569
574
  ----------
570
575
  df: 'pd.DataFrame'
571
576
  The dataframe to inspect and filter.
572
-
577
+
573
578
  safe_copy: bool, default True
574
579
  If `True`, create a copy before comparing and modifying the dataframes.
575
580
  Setting to `False` may mutate the DataFrames.
@@ -578,6 +583,10 @@ def filter_existing(
578
583
  date_bound_only: bool, default False
579
584
  If `True`, only use the datetime index to fetch the sample dataframe.
580
585
 
586
+ include_unchanged_columns: bool, default False
587
+ If `True`, include the backtrack columns which haven't changed in the update dataframe.
588
+ This is useful if you can't update individual keys.
589
+
581
590
  chunksize: Optional[int], default -1
582
591
  The `chunksize` used when fetching existing data.
583
592
 
@@ -605,7 +614,7 @@ def filter_existing(
605
614
  from meerschaum.config import get_config
606
615
  pd = import_pandas()
607
616
  pandas = attempt_import('pandas')
608
- if not 'dataframe' in str(type(df)).lower():
617
+ if 'dataframe' not in str(type(df)).lower():
609
618
  df = self.enforce_dtypes(df, chunksize=chunksize, debug=debug)
610
619
  is_dask = 'dask' in df.__module__
611
620
  if is_dask:
@@ -615,8 +624,21 @@ def filter_existing(
615
624
  else:
616
625
  merge = pd.merge
617
626
  NA = pd.NA
627
+
628
+ def get_empty_df():
629
+ empty_df = pd.DataFrame([])
630
+ dtypes = dict(df.dtypes) if df is not None else {}
631
+ dtypes.update(self.dtypes)
632
+ pd_dtypes = {
633
+ col: to_pandas_dtype(str(typ))
634
+ for col, typ in dtypes.items()
635
+ }
636
+ return add_missing_cols_to_df(empty_df, pd_dtypes)
637
+
618
638
  if df is None:
619
- return df, df, df
639
+ empty_df = get_empty_df()
640
+ return empty_df, empty_df, empty_df
641
+
620
642
  if (df.empty if not is_dask else len(df) == 0):
621
643
  return df, df, df
622
644
 
@@ -633,7 +655,7 @@ def filter_existing(
633
655
  if min_dt_val is not None and 'datetime' in str(dt_type)
634
656
  else min_dt_val
635
657
  )
636
- except Exception as e:
658
+ except Exception:
637
659
  min_dt = None
638
660
  if not ('datetime' in str(type(min_dt))) or str(min_dt) == 'NaT':
639
661
  if 'int' not in str(type(min_dt)).lower():
@@ -643,7 +665,7 @@ def filter_existing(
643
665
  begin = (
644
666
  round_time(
645
667
  min_dt,
646
- to = 'down'
668
+ to='down'
647
669
  ) - timedelta(minutes=1)
648
670
  )
649
671
  elif dt_type and 'int' in dt_type.lower():
@@ -661,7 +683,7 @@ def filter_existing(
661
683
  if max_dt_val is not None and 'datetime' in str(dt_type)
662
684
  else max_dt_val
663
685
  )
664
- except Exception as e:
686
+ except Exception:
665
687
  import traceback
666
688
  traceback.print_exc()
667
689
  max_dt = None
@@ -674,14 +696,14 @@ def filter_existing(
674
696
  end = (
675
697
  round_time(
676
698
  max_dt,
677
- to = 'down'
699
+ to='down'
678
700
  ) + timedelta(minutes=1)
679
701
  )
680
702
  elif dt_type and 'int' in dt_type.lower():
681
703
  end = max_dt + 1
682
704
 
683
705
  if max_dt is not None and min_dt is not None and min_dt > max_dt:
684
- warn(f"Detected minimum datetime greater than maximum datetime.")
706
+ warn("Detected minimum datetime greater than maximum datetime.")
685
707
 
686
708
  if begin is not None and end is not None and begin > end:
687
709
  if isinstance(begin, datetime):
@@ -710,13 +732,18 @@ def filter_existing(
710
732
  dprint(f"Looking at data between '{begin}' and '{end}':", **kw)
711
733
 
712
734
  backtrack_df = self.get_data(
713
- begin = begin,
714
- end = end,
715
- chunksize = chunksize,
716
- params = params,
717
- debug = debug,
735
+ begin=begin,
736
+ end=end,
737
+ chunksize=chunksize,
738
+ params=params,
739
+ debug=debug,
718
740
  **kw
719
741
  )
742
+ if backtrack_df is None:
743
+ if debug:
744
+ dprint(f"No backtrack data was found for {self}.")
745
+ return df, get_empty_df(), df
746
+
720
747
  if debug:
721
748
  dprint(f"Existing data for {self}:\n" + str(backtrack_df), **kw)
722
749
  dprint(f"Existing dtypes for {self}:\n" + str(backtrack_df.dtypes))
@@ -743,18 +770,19 @@ def filter_existing(
743
770
  filter_unseen_df(
744
771
  backtrack_df,
745
772
  df,
746
- dtypes = {
773
+ dtypes={
747
774
  col: to_pandas_dtype(typ)
748
775
  for col, typ in self_dtypes.items()
749
776
  },
750
- safe_copy = safe_copy,
751
- debug = debug
777
+ safe_copy=safe_copy,
778
+ debug=debug
752
779
  ),
753
780
  on_cols_dtypes,
754
781
  )
755
782
 
756
783
  ### Cast dicts or lists to strings so we can merge.
757
784
  serializer = functools.partial(json.dumps, sort_keys=True, separators=(',', ':'), default=str)
785
+
758
786
  def deserializer(x):
759
787
  return json.loads(x) if isinstance(x, str) else x
760
788
 
@@ -767,12 +795,12 @@ def filter_existing(
767
795
  casted_cols = set(unhashable_delta_cols + unhashable_backtrack_cols)
768
796
 
769
797
  joined_df = merge(
770
- delta_df.fillna(NA),
771
- backtrack_df.fillna(NA),
772
- how = 'left',
773
- on = on_cols,
774
- indicator = True,
775
- suffixes = ('', '_old'),
798
+ delta_df.infer_objects(copy=False).fillna(NA),
799
+ backtrack_df.infer_objects(copy=False).fillna(NA),
800
+ how='left',
801
+ on=on_cols,
802
+ indicator=True,
803
+ suffixes=('', '_old'),
776
804
  ) if on_cols else delta_df
777
805
  for col in casted_cols:
778
806
  if col in joined_df.columns:
@@ -782,20 +810,13 @@ def filter_existing(
782
810
 
783
811
  ### Determine which rows are completely new.
784
812
  new_rows_mask = (joined_df['_merge'] == 'left_only') if on_cols else None
785
- cols = list(backtrack_df.columns)
813
+ cols = list(delta_df.columns)
786
814
 
787
815
  unseen_df = (
788
- (
789
- joined_df
790
- .where(new_rows_mask)
791
- .dropna(how='all')[cols]
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
- )
816
+ joined_df
817
+ .where(new_rows_mask)
818
+ .dropna(how='all')[cols]
819
+ .reset_index(drop=True)
799
820
  ) if on_cols else delta_df
800
821
 
801
822
  ### Rows that have already been inserted but values have changed.
@@ -804,20 +825,33 @@ def filter_existing(
804
825
  .where(~new_rows_mask)
805
826
  .dropna(how='all')[cols]
806
827
  .reset_index(drop=True)
807
- ) if on_cols else None
828
+ ) if on_cols else get_empty_df()
829
+
830
+ if include_unchanged_columns and on_cols:
831
+ unchanged_backtrack_cols = [
832
+ col
833
+ for col in backtrack_df.columns
834
+ if col in on_cols or col not in update_df.columns
835
+ ]
836
+ update_df = merge(
837
+ backtrack_df[unchanged_backtrack_cols],
838
+ update_df,
839
+ how='inner',
840
+ on=on_cols,
841
+ )
808
842
 
809
843
  return unseen_df, update_df, delta_df
810
844
 
811
845
 
812
846
  @staticmethod
813
847
  def _get_chunk_label(
814
- chunk: Union[
815
- 'pd.DataFrame',
816
- List[Dict[str, Any]],
817
- Dict[str, List[Any]]
818
- ],
819
- dt_col: str,
820
- ) -> str:
848
+ chunk: Union[
849
+ 'pd.DataFrame',
850
+ List[Dict[str, Any]],
851
+ Dict[str, List[Any]]
852
+ ],
853
+ dt_col: str,
854
+ ) -> str:
821
855
  """
822
856
  Return the min - max label for the chunk.
823
857
  """
@@ -870,3 +904,52 @@ def get_num_workers(self, workers: Optional[int] = None) -> int:
870
904
  (desired_workers - current_num_connections),
871
905
  1,
872
906
  )
907
+
908
+
909
+ def _persist_new_numeric_columns(self, df, debug: bool = False) -> SuccessTuple:
910
+ """
911
+ Check for new numeric columns and update the parameters.
912
+ """
913
+ from meerschaum.utils.dataframe import get_numeric_cols
914
+ numeric_cols = get_numeric_cols(df)
915
+ existing_numeric_cols = [col for col, typ in self.dtypes.items() if typ == 'numeric']
916
+ new_numeric_cols = [col for col in numeric_cols if col not in existing_numeric_cols]
917
+ if not new_numeric_cols:
918
+ return True, "Success"
919
+
920
+ dtypes = self.parameters.get('dtypes', {})
921
+ dtypes.update({col: 'numeric' for col in numeric_cols})
922
+ self.parameters['dtypes'] = dtypes
923
+ if not self.temporary:
924
+ edit_success, edit_msg = self.edit(interactive=False, debug=debug)
925
+ if not edit_success:
926
+ warn(f"Unable to update NUMERIC dtypes for {self}:\n{edit_msg}")
927
+
928
+ return edit_success, edit_msg
929
+
930
+ return True, "Success"
931
+
932
+
933
+ def _persist_new_json_columns(self, df, debug: bool = False) -> SuccessTuple:
934
+ """
935
+ Check for new JSON columns and update the parameters.
936
+ """
937
+ from meerschaum.utils.dataframe import get_json_cols
938
+ json_cols = get_json_cols(df)
939
+ existing_json_cols = [col for col, typ in self.dtypes.items() if typ == 'json']
940
+ new_json_cols = [col for col in json_cols if col not in existing_json_cols]
941
+ if not new_json_cols:
942
+ return True, "Success"
943
+
944
+ dtypes = self.parameters.get('dtypes', {})
945
+ dtypes.update({col: 'json' for col in json_cols})
946
+ self.parameters['dtypes'] = dtypes
947
+
948
+ if not self.temporary:
949
+ edit_success, edit_msg = self.edit(interactive=False, debug=debug)
950
+ if not edit_success:
951
+ warn(f"Unable to update JSON dtypes for {self}:\n{edit_msg}")
952
+
953
+ return edit_success, edit_msg
954
+
955
+ return True, "Success"
@@ -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, Tuple
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
- password: str,
23
- salt: Optional[bytes] = None,
24
- rounds: Optional[int] = None,
25
- ) -> str:
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
- password: str,
72
- password_hash: str,
73
- ) -> bool:
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) -> meerschaum.connectors.Connector:
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
@@ -60,9 +60,10 @@ class Job:
60
60
  sysargs: Union[List[str], str, None] = None,
61
61
  env: Optional[Dict[str, str]] = None,
62
62
  executor_keys: Optional[str] = None,
63
+ delete_after_completion: bool = False,
63
64
  _properties: Optional[Dict[str, Any]] = None,
64
- _rotating_log = None,
65
- _stdin_file = None,
65
+ _rotating_log=None,
66
+ _stdin_file=None,
66
67
  _status_hook: Optional[Callable[[], str]] = None,
67
68
  _result_hook: Optional[Callable[[], SuccessTuple]] = None,
68
69
  _externally_managed: bool = False,
@@ -85,6 +86,9 @@ class Job:
85
86
  executor_keys: Optional[str], default None
86
87
  If provided, execute the job remotely on an API instance, e.g. 'api:main'.
87
88
 
89
+ delete_after_completion: bool, default False
90
+ If `True`, delete this job when it has finished executing.
91
+
88
92
  _properties: Optional[Dict[str, Any]], default None
89
93
  If provided, use this to patch the daemon's properties.
90
94
  """
@@ -146,6 +150,9 @@ class Job:
146
150
  if env:
147
151
  self._properties_patch.update({'env': env})
148
152
 
153
+ if delete_after_completion:
154
+ self._properties_patch.update({'delete_after_completion': delete_after_completion})
155
+
149
156
  daemon_sysargs = (
150
157
  self._daemon.properties.get('target', {}).get('args', [None])[0]
151
158
  if self._daemon is not None
@@ -245,7 +252,7 @@ class Job:
245
252
  return True, f"{self} is already running."
246
253
 
247
254
  success, msg = self.daemon.run(
248
- keep_daemon_output=True,
255
+ keep_daemon_output=(not self.delete_after_completion),
249
256
  allow_dirty_run=True,
250
257
  )
251
258
  if not success:
@@ -407,7 +414,6 @@ class Job:
407
414
  )
408
415
  return asyncio.run(monitor_logs_coroutine)
409
416
 
410
-
411
417
  async def monitor_logs_async(
412
418
  self,
413
419
  callback_function: Callable[[str], None] = partial(print, end='', flush=True),
@@ -418,8 +424,8 @@ class Job:
418
424
  strip_timestamps: bool = False,
419
425
  accept_input: bool = True,
420
426
  _logs_path: Optional[pathlib.Path] = None,
421
- _log = None,
422
- _stdin_file = None,
427
+ _log=None,
428
+ _stdin_file=None,
423
429
  debug: bool = False,
424
430
  ):
425
431
  """
@@ -466,6 +472,7 @@ class Job:
466
472
  input_callback_function=input_callback_function,
467
473
  stop_callback_function=stop_callback_function,
468
474
  stop_on_exit=stop_on_exit,
475
+ strip_timestamps=strip_timestamps,
469
476
  accept_input=accept_input,
470
477
  debug=debug,
471
478
  )
@@ -557,7 +564,6 @@ class Job:
557
564
  for task in pending:
558
565
  task.cancel()
559
566
  except asyncio.exceptions.CancelledError:
560
- print('cancelled?')
561
567
  pass
562
568
  finally:
563
569
  combined_event.set()
@@ -870,7 +876,9 @@ class Job:
870
876
  """
871
877
  Return the job's Daemon label (joined sysargs).
872
878
  """
873
- return shlex.join(self.sysargs).replace(' + ', '\n+ ')
879
+ from meerschaum._internal.arguments import compress_pipeline_sysargs
880
+ sysargs = compress_pipeline_sysargs(self.sysargs)
881
+ return shlex.join(sysargs).replace(' + ', '\n+ ')
874
882
 
875
883
  @property
876
884
  def _externally_managed_file(self) -> pathlib.Path:
@@ -916,6 +924,16 @@ class Job:
916
924
  self._env = {**default_env, **_env}
917
925
  return self._env
918
926
 
927
+ @property
928
+ def delete_after_completion(self) -> bool:
929
+ """
930
+ Return whether this job is configured to delete itself after completion.
931
+ """
932
+ if '_delete_after_completion' in self.__dict__:
933
+ return self.__dict__.get('_delete_after_completion', False)
934
+
935
+ self._delete_after_completion = self.daemon.properties.get('delete_after_completion', False)
936
+ return self._delete_after_completion
919
937
 
920
938
  def __str__(self) -> str:
921
939
  sysargs = self.sysargs
@@ -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 service_name.startswith('mrsm-') and service_name.endswith('.service')
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
- while (time.perf_counter() - loop_start) < get_config('jobs', 'timeout_seconds'):
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
- _ = self.stop_job(name, debug=debug)
635
- _ = self.run_command(
636
- ['disable', self.get_service_name(name, debug=debug)],
637
- debug=debug,
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)
@@ -7,8 +7,12 @@ Plugin metadata class
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
- import os, pathlib, shutil
11
- from site import venv
10
+
11
+ import os
12
+ import pathlib
13
+ import shutil
14
+
15
+ import meerschaum as mrsm
12
16
  from meerschaum.utils.typing import (
13
17
  Dict,
14
18
  List,
@@ -40,8 +44,8 @@ class Plugin:
40
44
  attributes: Optional[Dict[str, Any]] = None,
41
45
  archive_path: Optional[pathlib.Path] = None,
42
46
  venv_path: Optional[pathlib.Path] = None,
43
- repo_connector: Optional['meerschaum.connectors.api.APIConnector'] = None,
44
- repo: Union['meerschaum.connectors.api.APIConnector', str, None] = None,
47
+ repo_connector: Optional['mrsm.connectors.api.APIConnector'] = None,
48
+ repo: Union['mrsm.connectors.api.APIConnector', str, None] = None,
45
49
  ):
46
50
  from meerschaum.config.static import STATIC_CONFIG
47
51
  sep = STATIC_CONFIG['plugins']['repo_separator']
@@ -470,9 +474,9 @@ class Plugin:
470
474
 
471
475
 
472
476
  def remove_archive(
473
- self,
474
- debug: bool = False
475
- ) -> SuccessTuple:
477
+ self,
478
+ debug: bool = False
479
+ ) -> SuccessTuple:
476
480
  """Remove a plugin's archive file."""
477
481
  if not self.archive_path.exists():
478
482
  return True, f"Archive file for plugin '{self}' does not exist."
@@ -484,9 +488,9 @@ class Plugin:
484
488
 
485
489
 
486
490
  def remove_venv(
487
- self,
488
- debug: bool = False
489
- ) -> SuccessTuple:
491
+ self,
492
+ debug: bool = False
493
+ ) -> SuccessTuple:
490
494
  """Remove a plugin's virtual environment."""
491
495
  if not self.venv_path.exists():
492
496
  return True, f"Virtual environment for plugin '{self}' does not exist."
@@ -608,9 +612,9 @@ class Plugin:
608
612
 
609
613
 
610
614
  def get_dependencies(
611
- self,
612
- debug: bool = False,
613
- ) -> List[str]:
615
+ self,
616
+ debug: bool = False,
617
+ ) -> List[str]:
614
618
  """
615
619
  If the Plugin has specified dependencies in a list called `required`, return the list.
616
620