meerschaum 2.4.10__py3-none-any.whl → 2.4.12__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 (33) hide show
  1. meerschaum/_internal/arguments/_parse_arguments.py +15 -1
  2. meerschaum/_internal/docs/index.py +1 -0
  3. meerschaum/_internal/shell/Shell.py +19 -9
  4. meerschaum/_internal/shell/ShellCompleter.py +11 -6
  5. meerschaum/actions/bootstrap.py +120 -15
  6. meerschaum/actions/clear.py +41 -30
  7. meerschaum/actions/edit.py +89 -0
  8. meerschaum/actions/start.py +3 -2
  9. meerschaum/actions/sync.py +3 -2
  10. meerschaum/api/dash/callbacks/dashboard.py +2 -1
  11. meerschaum/api/dash/callbacks/jobs.py +53 -7
  12. meerschaum/api/dash/callbacks/pipes.py +1 -1
  13. meerschaum/api/dash/jobs.py +86 -60
  14. meerschaum/api/dash/pages/__init__.py +1 -0
  15. meerschaum/api/dash/pages/job.py +21 -0
  16. meerschaum/api/routes/_jobs.py +3 -3
  17. meerschaum/config/_version.py +1 -1
  18. meerschaum/connectors/sql/_fetch.py +65 -61
  19. meerschaum/connectors/sql/_pipes.py +36 -29
  20. meerschaum/core/Pipe/_data.py +1 -1
  21. meerschaum/utils/formatting/__init__.py +32 -16
  22. meerschaum/utils/formatting/_pipes.py +1 -1
  23. meerschaum/utils/formatting/_shell.py +4 -3
  24. meerschaum/utils/prompt.py +16 -15
  25. meerschaum/utils/sql.py +107 -35
  26. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/METADATA +1 -1
  27. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/RECORD +33 -32
  28. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/WHEEL +1 -1
  29. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/LICENSE +0 -0
  30. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/NOTICE +0 -0
  31. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/entry_points.txt +0 -0
  32. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/top_level.txt +0 -0
  33. {meerschaum-2.4.10.dist-info → meerschaum-2.4.12.dist-info}/zip-safe +0 -0
@@ -12,6 +12,7 @@ import json
12
12
  import time
13
13
  import traceback
14
14
  from datetime import datetime, timezone
15
+
15
16
  from meerschaum.utils.typing import Optional, Dict, Any
16
17
  from meerschaum.api import CHECK_UPDATE
17
18
  from meerschaum.api.dash import dash_app
@@ -19,17 +20,19 @@ from meerschaum.api.dash.sessions import get_username_from_session
19
20
  from meerschaum.utils.packages import attempt_import, import_dcc, import_html
20
21
  from meerschaum.api.dash.components import alert_from_success_tuple
21
22
  from meerschaum.api.dash.jobs import (
23
+ build_job_card,
22
24
  build_manage_job_buttons_div_children,
23
25
  build_status_children,
24
26
  build_process_timestamps_children,
25
27
  )
26
- from meerschaum.jobs import Job
28
+ from meerschaum.api.routes._jobs import _get_job
27
29
  from meerschaum.api.dash.sessions import is_session_authenticated
28
30
  dash = attempt_import('dash', lazy=False, check_update=CHECK_UPDATE)
29
31
  html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
30
32
  from dash.exceptions import PreventUpdate
31
33
  from dash.dependencies import Input, Output, State, ALL, MATCH
32
34
  import dash_bootstrap_components as dbc
35
+ from dash import no_update
33
36
 
34
37
 
35
38
  @dash_app.callback(
@@ -53,6 +56,13 @@ def download_job_logs(n_clicks):
53
56
 
54
57
  component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
55
58
  job_name = component_dict['index']
59
+ try:
60
+ job = _get_job(job_name)
61
+ except Exception:
62
+ job = None
63
+ if job is None or not job.exists():
64
+ raise PreventUpdate
65
+
56
66
  now = datetime.now(timezone.utc)
57
67
  filename = job_name + '_' + str(int(now.timestamp())) + '.log'
58
68
  return {
@@ -69,7 +79,7 @@ def download_job_logs(n_clicks):
69
79
  Input({'type': 'manage-job-button', 'action': ALL, 'index': MATCH}, 'n_clicks'),
70
80
  State('session-store', 'data'),
71
81
  State({'type': 'job-label-p', 'index': MATCH}, 'children'),
72
- prevent_initial_call = True,
82
+ prevent_initial_call=True,
73
83
  )
74
84
  def manage_job_button_click(
75
85
  n_clicks: Optional[int] = None,
@@ -102,10 +112,10 @@ def manage_job_button_click(
102
112
  job_name = component_dict['index']
103
113
  manage_job_action = component_dict['action']
104
114
  try:
105
- job = Job(job_name, job_label.replace('\n', ' ') if job_label else None)
106
- except Exception as e:
115
+ job = _get_job(job_name, job_label.replace('\n', ' ') if job_label else None)
116
+ except Exception:
107
117
  job = None
108
- if job is None:
118
+ if job is None or not job.exists():
109
119
  raise PreventUpdate
110
120
 
111
121
  manage_functions = {
@@ -191,7 +201,7 @@ dash_app.clientside_callback(
191
201
  Output({'type': 'process-timestamps-div', 'index': ALL}, 'children'),
192
202
  Input('refresh-jobs-interval', 'n_intervals'),
193
203
  State('session-store', 'data'),
194
- prevent_initial_call = True,
204
+ prevent_initial_call=True,
195
205
  )
196
206
  def refresh_jobs_on_interval(
197
207
  n_intervals: Optional[int] = None,
@@ -209,7 +219,7 @@ def refresh_jobs_on_interval(
209
219
  ]
210
220
 
211
221
  ### NOTE: The job may have been deleted, but the card may still exist.
212
- jobs = [Job(name) for name in job_names]
222
+ jobs = [_get_job(name) for name in job_names]
213
223
 
214
224
  return (
215
225
  [
@@ -229,3 +239,39 @@ def refresh_jobs_on_interval(
229
239
  for job in jobs
230
240
  ],
231
241
  )
242
+
243
+
244
+ @dash_app.callback(
245
+ Output('job-output-div', 'children'),
246
+ Input('job-location', 'pathname'),
247
+ State('session-store', 'data'),
248
+ )
249
+ def render_job_page_from_url(
250
+ pathname: str,
251
+ session_data: Optional[Dict[str, Any]],
252
+ ):
253
+ """
254
+ Load the `/job/{name}` page.
255
+ """
256
+ if not str(pathname).startswith('/dash/job'):
257
+ return no_update
258
+
259
+ session_id = (session_data or {}).get('session-id', None)
260
+ authenticated = is_session_authenticated(str(session_id))
261
+
262
+ job_name = pathname.replace('/dash/job', '').lstrip('/').rstrip('/')
263
+ if not job_name:
264
+ return no_update
265
+
266
+ job = _get_job(job_name)
267
+ if not job.exists():
268
+ return [
269
+ html.Br(),
270
+ html.H2("404: Job does not exist."),
271
+ ]
272
+
273
+ return [
274
+ html.Br(),
275
+ build_job_card(job, authenticated=authenticated, include_follow=False),
276
+ html.Br(),
277
+ ]
@@ -26,7 +26,7 @@ html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHEC
26
26
  State('pipes-location', 'search'),
27
27
  State('session-store', 'data'),
28
28
  )
29
- def render_page_from_url(
29
+ def render_pipe_page_from_url(
30
30
  pathname: str,
31
31
  pipe_search: str,
32
32
  session_data: Optional[Dict[str, Any]],
@@ -31,6 +31,7 @@ STATUS_EMOJI: Dict[str, str] = {
31
31
 
32
32
  EXECUTOR_KEYS: str = get_executor_keys_from_context()
33
33
 
34
+
34
35
  def get_jobs_cards(state: WebState):
35
36
  """
36
37
  Build cards and alerts lists for jobs.
@@ -42,71 +43,96 @@ def get_jobs_cards(state: WebState):
42
43
  cards = []
43
44
 
44
45
  for name, job in jobs.items():
45
- footer_children = html.Div(
46
- build_process_timestamps_children(job),
47
- id = {'type': 'process-timestamps-div', 'index': name},
48
- )
49
- follow_logs_button = dbc.DropdownMenuItem(
50
- "Follow logs",
51
- id = {'type': 'follow-logs-button', 'index': name},
52
- )
53
- download_logs_button = dbc.DropdownMenuItem(
54
- "Download logs",
55
- id = {'type': 'job-download-logs-button', 'index': name},
56
- )
57
- logs_menu_children = (
58
- ([follow_logs_button] if is_authenticated else []) + [download_logs_button]
59
- )
60
- header_children = [
61
- html.Div(
62
- build_status_children(job),
63
- id={'type': 'manage-job-status-div', 'index': name},
64
- style={'float': 'left'},
46
+ cards.append(build_job_card(job, authenticated=is_authenticated))
47
+
48
+ return cards, []
49
+
50
+
51
+ def build_job_card(
52
+ job: Job,
53
+ authenticated: bool = False,
54
+ include_follow: bool = True,
55
+ ):
56
+ """
57
+ Return a card for a given job.
58
+ """
59
+ footer_children = html.Div(
60
+ build_process_timestamps_children(job),
61
+ id={'type': 'process-timestamps-div', 'index': job.name},
62
+ )
63
+ follow_logs_button = dbc.DropdownMenuItem(
64
+ "Follow logs",
65
+ id={'type': 'follow-logs-button', 'index': job.name},
66
+ )
67
+ download_logs_button = dbc.DropdownMenuItem(
68
+ "Download logs",
69
+ id={'type': 'job-download-logs-button', 'index': job.name},
70
+ )
71
+ logs_menu_children = (
72
+ ([follow_logs_button] if include_follow else []) + [download_logs_button]
73
+ if authenticated
74
+ else []
75
+ )
76
+ header_children = [
77
+ html.Div(
78
+ build_status_children(job),
79
+ id={'type': 'manage-job-status-div', 'index': job.name},
80
+ style={'float': 'left'},
81
+ ),
82
+ ] + ([
83
+ html.Div(
84
+ dbc.DropdownMenu(
85
+ logs_menu_children,
86
+ label="Logs",
87
+ size="sm",
88
+ align_end=True,
89
+ color="secondary",
90
+ menu_variant='dark',
65
91
  ),
66
- html.Div(
67
- dbc.DropdownMenu(
68
- logs_menu_children,
69
- label="Logs",
70
- size="sm",
71
- align_end=True,
72
- color="secondary",
73
- menu_variant='dark',
74
- ),
75
- style={'float': 'right'},
92
+ style={'float': 'right'},
93
+ ),
94
+ ] if authenticated else [])
95
+
96
+ body_children = [
97
+ html.H4(
98
+ html.B(
99
+ html.A(
100
+ "🔗 " + job.name,
101
+ href=f"/dash/job/{job.name}",
102
+ target="_blank",
103
+ style={
104
+ 'color': 'white',
105
+ 'text-decoration': 'none',
106
+ },
107
+ )
76
108
  ),
77
- ]
78
-
79
- body_children = [
80
- html.H4(html.B(name), className="card-title"),
81
- html.Div(
82
- html.P(
83
- job.label,
84
- className="card-text job-card-text",
85
- style={"word-wrap": "break-word"},
86
- id={'type': 'job-label-p', 'index': name},
87
- ),
88
- style={"white-space": "pre-wrap"},
109
+ className="card-title",
110
+ ),
111
+ html.Div(
112
+ html.P(
113
+ job.label,
114
+ className="card-text job-card-text",
115
+ style={"word-wrap": "break-word"},
116
+ id={'type': 'job-label-p', 'index': job.name},
89
117
  ),
90
- html.Div(
91
- (
92
- build_manage_job_buttons_div_children(job)
93
- if is_authenticated
94
- else []
95
- ),
96
- id={'type': 'manage-job-buttons-div', 'index': name},
118
+ style={"white-space": "pre-wrap"},
119
+ ),
120
+ html.Div(
121
+ (
122
+ build_manage_job_buttons_div_children(job)
123
+ if authenticated
124
+ else []
97
125
  ),
98
- html.Div(id={'type': 'manage-job-alert-div', 'index': name}),
99
- ]
100
-
101
- cards.append(
102
- dbc.Card([
103
- dbc.CardHeader(header_children),
104
- dbc.CardBody(body_children),
105
- dbc.CardFooter(footer_children),
106
- ])
107
- )
126
+ id={'type': 'manage-job-buttons-div', 'index': job.name},
127
+ ),
128
+ html.Div(id={'type': 'manage-job-alert-div', 'index': job.name}),
129
+ ]
108
130
 
109
- return cards, []
131
+ return dbc.Card([
132
+ dbc.CardHeader(header_children),
133
+ dbc.CardBody(body_children),
134
+ dbc.CardFooter(footer_children),
135
+ ])
110
136
 
111
137
 
112
138
  def build_manage_job_buttons_div_children(job: Job):
@@ -11,3 +11,4 @@ import meerschaum.api.dash.pages.dashboard
11
11
  import meerschaum.api.dash.pages.plugins
12
12
  import meerschaum.api.dash.pages.register
13
13
  import meerschaum.api.dash.pages.pipes
14
+ import meerschaum.api.dash.pages.job
@@ -0,0 +1,21 @@
1
+ #! /usr/bin/env python3
2
+ # vim:fenc=utf-8
3
+
4
+ """
5
+ Display pipes via a shareable URL.
6
+ """
7
+
8
+ from meerschaum.api import CHECK_UPDATE
9
+ from meerschaum.utils.packages import import_html, import_dcc
10
+
11
+ html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
12
+ import dash_bootstrap_components as dbc
13
+
14
+ from meerschaum.api.dash.components import download_logs, refresh_jobs_interval
15
+
16
+ layout = dbc.Container([
17
+ dcc.Location('job-location'),
18
+ html.Div(id='job-output-div'),
19
+ download_logs,
20
+ refresh_jobs_interval,
21
+ ])
@@ -39,12 +39,12 @@ NONINTERACTIVE_ENV: str = STATIC_CONFIG['environment']['noninteractive']
39
39
  EXECUTOR_KEYS: str = 'local'
40
40
 
41
41
 
42
- def _get_job(name: str):
43
- systemd_job = Job(name, executor_keys='systemd')
42
+ def _get_job(name: str, sysargs: Union[str, List[str], None] = None):
43
+ systemd_job = Job(name, sysargs, executor_keys='systemd')
44
44
  if systemd_job.exists():
45
45
  return systemd_job
46
46
 
47
- job = Job(name, executor_keys=EXECUTOR_KEYS)
47
+ job = Job(name, sysargs, executor_keys=EXECUTOR_KEYS)
48
48
  return job
49
49
 
50
50
 
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.4.10"
5
+ __version__ = "2.4.12"
@@ -7,27 +7,29 @@ Implement the Connector fetch() method
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+
10
11
  from datetime import datetime, timedelta
11
12
  import meerschaum as mrsm
12
- from meerschaum.utils.typing import Optional, Union, Callable, Any
13
+ from meerschaum.utils.typing import Optional, Union, Callable, Any, List, Dict
14
+
13
15
 
14
16
  def fetch(
15
- self,
16
- pipe: meerschaum.Pipe,
17
- begin: Union[datetime, int, str, None] = '',
18
- end: Union[datetime, int, str, None] = None,
19
- check_existing: bool = True,
20
- chunk_hook: Optional[Callable[[pd.DataFrame], Any]] = None,
21
- chunksize: Optional[int] = -1,
22
- workers: Optional[int] = None,
23
- debug: bool = False,
24
- **kw: Any
25
- ) -> Union['pd.DataFrame', List[Any], None]:
17
+ self,
18
+ pipe: mrsm.Pipe,
19
+ begin: Union[datetime, int, str, None] = '',
20
+ end: Union[datetime, int, str, None] = None,
21
+ check_existing: bool = True,
22
+ chunk_hook: Optional[Callable[['pd.DataFrame'], Any]] = None,
23
+ chunksize: Optional[int] = -1,
24
+ workers: Optional[int] = None,
25
+ debug: bool = False,
26
+ **kw: Any
27
+ ) -> Union['pd.DataFrame', List[Any], None]:
26
28
  """Execute the SQL definition and return a Pandas DataFrame.
27
29
 
28
30
  Parameters
29
31
  ----------
30
- pipe: meerschaum.Pipe
32
+ pipe: mrsm.Pipe
31
33
  The pipe object which contains the `fetch` metadata.
32
34
 
33
35
  - pipe.columns['datetime']: str
@@ -63,7 +65,7 @@ def fetch(
63
65
 
64
66
  debug: bool, default False
65
67
  Verbosity toggle.
66
-
68
+
67
69
  Returns
68
70
  -------
69
71
  A pandas DataFrame or `None`.
@@ -71,20 +73,20 @@ def fetch(
71
73
  """
72
74
  meta_def = self.get_pipe_metadef(
73
75
  pipe,
74
- begin = begin,
75
- end = end,
76
- check_existing = check_existing,
77
- debug = debug,
76
+ begin=begin,
77
+ end=end,
78
+ check_existing=check_existing,
79
+ debug=debug,
78
80
  **kw
79
81
  )
80
82
  as_hook_results = chunk_hook is not None
81
83
  chunks = self.read(
82
84
  meta_def,
83
- chunk_hook = chunk_hook,
84
- as_hook_results = as_hook_results,
85
- chunksize = chunksize,
86
- workers = workers,
87
- debug = debug,
85
+ chunk_hook=chunk_hook,
86
+ as_hook_results=as_hook_results,
87
+ chunksize=chunksize,
88
+ workers=workers,
89
+ debug=debug,
88
90
  )
89
91
  ### if sqlite, parse for datetimes
90
92
  if not as_hook_results and self.flavor == 'sqlite':
@@ -97,8 +99,8 @@ def fetch(
97
99
  return (
98
100
  parse_df_datetimes(
99
101
  chunk,
100
- ignore_cols = ignore_cols,
101
- debug = debug,
102
+ ignore_cols=ignore_cols,
103
+ debug=debug,
102
104
  )
103
105
  for chunk in chunks
104
106
  )
@@ -106,15 +108,15 @@ def fetch(
106
108
 
107
109
 
108
110
  def get_pipe_metadef(
109
- self,
110
- pipe: meerschaum.Pipe,
111
- params: Optional[Dict[str, Any]] = None,
112
- begin: Union[datetime, int, str, None] = '',
113
- end: Union[datetime, int, str, None] = None,
114
- check_existing: bool = True,
115
- debug: bool = False,
116
- **kw: Any
117
- ) -> Union[str, None]:
111
+ self,
112
+ pipe: mrsm.Pipe,
113
+ params: Optional[Dict[str, Any]] = None,
114
+ begin: Union[datetime, int, str, None] = '',
115
+ end: Union[datetime, int, str, None] = None,
116
+ check_existing: bool = True,
117
+ debug: bool = False,
118
+ **kw: Any
119
+ ) -> Union[str, None]:
118
120
  """
119
121
  Return a pipe's meta definition fetch query.
120
122
 
@@ -173,7 +175,6 @@ def get_pipe_metadef(
173
175
  stack = False
174
176
  )
175
177
 
176
-
177
178
  apply_backtrack = begin == '' and check_existing
178
179
  backtrack_interval = pipe.get_backtrack_interval(check_existing=check_existing, debug=debug)
179
180
  btm = (
@@ -189,35 +190,34 @@ def get_pipe_metadef(
189
190
 
190
191
  if begin and end and begin >= end:
191
192
  begin = None
192
-
193
- da = None
193
+
194
194
  if dt_name:
195
195
  begin_da = (
196
196
  dateadd_str(
197
- flavor = self.flavor,
198
- datepart = 'minute',
199
- number = ((-1 * btm) if apply_backtrack else 0),
200
- begin = begin,
197
+ flavor=self.flavor,
198
+ datepart='minute',
199
+ number=((-1 * btm) if apply_backtrack else 0),
200
+ begin=begin,
201
201
  )
202
202
  if begin
203
203
  else None
204
204
  )
205
205
  end_da = (
206
206
  dateadd_str(
207
- flavor = self.flavor,
208
- datepart = 'minute',
209
- number = 0,
210
- begin = end,
207
+ flavor=self.flavor,
208
+ datepart='minute',
209
+ number=0,
210
+ begin=end,
211
211
  )
212
212
  if end
213
213
  else None
214
214
  )
215
215
 
216
216
  meta_def = (
217
- _simple_fetch_query(pipe) if (
217
+ _simple_fetch_query(pipe, self.flavor) if (
218
218
  (not (pipe.columns or {}).get('id', None))
219
219
  or (not get_config('system', 'experimental', 'join_fetch'))
220
- ) else _join_fetch_query(pipe, debug=debug, **kw)
220
+ ) else _join_fetch_query(pipe, self.flavor, debug=debug, **kw)
221
221
  )
222
222
 
223
223
  has_where = 'where' in meta_def.lower()[meta_def.lower().rfind('definition'):]
@@ -300,25 +300,28 @@ def set_pipe_query(pipe: mrsm.Pipe, query: str) -> None:
300
300
  dict_to_set[key_to_set] = query
301
301
 
302
302
 
303
- def _simple_fetch_query(pipe, debug: bool=False, **kw) -> str:
303
+ def _simple_fetch_query(
304
+ pipe: mrsm.Pipe,
305
+ flavor: str,
306
+ debug: bool = False,
307
+ **kw
308
+ ) -> str:
304
309
  """Build a fetch query from a pipe's definition."""
305
- def_name = 'definition'
310
+ from meerschaum.utils.sql import format_cte_subquery
306
311
  definition = get_pipe_query(pipe)
307
- return (
308
- f"WITH {def_name} AS (\n{definition}\n) SELECT * FROM {def_name}"
309
- if pipe.connector.flavor not in ('mysql', 'mariadb')
310
- else f"SELECT * FROM (\n{definition}\n) AS {def_name}"
311
- )
312
+ return format_cte_subquery(definition, flavor, 'definition')
313
+
312
314
 
313
315
  def _join_fetch_query(
314
- pipe,
315
- debug: bool = False,
316
- new_ids: bool = True,
317
- **kw
318
- ) -> str:
316
+ pipe: mrsm.Pipe,
317
+ flavor: str,
318
+ debug: bool = False,
319
+ new_ids: bool = True,
320
+ **kw
321
+ ) -> str:
319
322
  """Build a fetch query based on the datetime and ID indices."""
320
323
  if not pipe.exists(debug=debug):
321
- return _simple_fetch_query(pipe, debug=debug, **kw)
324
+ return _simple_fetch_query(pipe, flavor, debug=debug, **kw)
322
325
 
323
326
  from meerschaum.utils.sql import sql_item_name, dateadd_str
324
327
  pipe_instance_name = sql_item_name(
@@ -350,7 +353,8 @@ def _join_fetch_query(
350
353
  """
351
354
  sync_times = pipe.instance_connector.read(sync_times_query, debug=debug, silent=False)
352
355
  if sync_times is None:
353
- return _simple_fetch_query(pipe, debug=debug, **kw)
356
+ return _simple_fetch_query(pipe, flavor, debug=debug, **kw)
357
+
354
358
  _sync_times_q = f",\n{sync_times_remote_name} AS ("
355
359
  for _id, _st in sync_times.itertuples(index=False):
356
360
  _sync_times_q += (