meerschaum 2.1.7__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.
Files changed (59) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/_internal/arguments/_parser.py +3 -0
  3. meerschaum/_internal/entry.py +3 -2
  4. meerschaum/actions/install.py +7 -3
  5. meerschaum/actions/show.py +128 -42
  6. meerschaum/actions/sync.py +7 -3
  7. meerschaum/api/__init__.py +24 -14
  8. meerschaum/api/_oauth2.py +4 -4
  9. meerschaum/api/dash/callbacks/dashboard.py +93 -23
  10. meerschaum/api/dash/callbacks/jobs.py +55 -3
  11. meerschaum/api/dash/jobs.py +34 -8
  12. meerschaum/api/dash/keys.py +1 -1
  13. meerschaum/api/dash/pages/dashboard.py +14 -4
  14. meerschaum/api/dash/pipes.py +137 -26
  15. meerschaum/api/dash/plugins.py +25 -9
  16. meerschaum/api/resources/static/js/xterm.js +1 -1
  17. meerschaum/api/resources/templates/termpage.html +3 -0
  18. meerschaum/api/routes/_login.py +5 -4
  19. meerschaum/api/routes/_plugins.py +6 -3
  20. meerschaum/config/_dash.py +11 -0
  21. meerschaum/config/_default.py +3 -1
  22. meerschaum/config/_jobs.py +13 -4
  23. meerschaum/config/_paths.py +2 -0
  24. meerschaum/config/_sync.py +2 -3
  25. meerschaum/config/_version.py +1 -1
  26. meerschaum/config/stack/__init__.py +6 -7
  27. meerschaum/config/stack/grafana/__init__.py +1 -1
  28. meerschaum/config/static/__init__.py +4 -1
  29. meerschaum/connectors/__init__.py +2 -0
  30. meerschaum/connectors/api/_plugins.py +2 -1
  31. meerschaum/connectors/sql/SQLConnector.py +4 -2
  32. meerschaum/connectors/sql/_create_engine.py +9 -9
  33. meerschaum/connectors/sql/_instance.py +3 -1
  34. meerschaum/connectors/sql/_pipes.py +54 -38
  35. meerschaum/connectors/sql/_plugins.py +0 -2
  36. meerschaum/connectors/sql/_sql.py +7 -9
  37. meerschaum/core/User/_User.py +158 -16
  38. meerschaum/core/User/__init__.py +1 -1
  39. meerschaum/plugins/_Plugin.py +12 -3
  40. meerschaum/plugins/__init__.py +23 -1
  41. meerschaum/utils/daemon/Daemon.py +89 -36
  42. meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
  43. meerschaum/utils/daemon/RotatingFile.py +130 -14
  44. meerschaum/utils/daemon/__init__.py +3 -0
  45. meerschaum/utils/dtypes/__init__.py +9 -5
  46. meerschaum/utils/packages/__init__.py +21 -5
  47. meerschaum/utils/packages/_packages.py +18 -20
  48. meerschaum/utils/process.py +13 -10
  49. meerschaum/utils/schedule.py +276 -30
  50. meerschaum/utils/threading.py +1 -0
  51. meerschaum/utils/typing.py +1 -1
  52. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
  53. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/RECORD +59 -57
  54. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
  55. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
  56. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
  57. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
  58. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
  59. {meerschaum-2.1.7.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
@@ -11,6 +11,7 @@ import json
11
11
  import functools
12
12
  import time
13
13
  import traceback
14
+ from datetime import datetime, timezone
14
15
  import meerschaum as mrsm
15
16
  from meerschaum.utils.typing import Optional, Dict, Any
16
17
  from meerschaum.api import get_api_connector, endpoints, CHECK_UPDATE
@@ -54,9 +55,14 @@ def download_job_logs(n_clicks):
54
55
  component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
55
56
  daemon_id = component_dict['index']
56
57
  daemon = Daemon(daemon_id=daemon_id)
58
+ now = datetime.now(timezone.utc)
59
+ filename = (
60
+ daemon.rotating_log.file_path.name[:(-1 * len('.log'))]
61
+ + '_' + str(int(now.timestamp())) + '.log'
62
+ )
57
63
  return {
58
64
  'content': daemon.log_text,
59
- 'filename': daemon.rotating_log.file_path.name,
65
+ 'filename': filename,
60
66
  }
61
67
 
62
68
 
@@ -74,7 +80,7 @@ def manage_job_button_click(
74
80
  session_data: Optional[Dict[str, Any]] = None,
75
81
  ):
76
82
  """
77
- Start, stop, or pause the given job.
83
+ Start, stop, pause, or delete the given job.
78
84
  """
79
85
  if not n_clicks:
80
86
  raise PreventUpdate
@@ -98,12 +104,18 @@ def manage_job_button_click(
98
104
  component_dict = json.loads(ctx[0]['prop_id'].split('.' + 'n_clicks')[0])
99
105
  daemon_id = component_dict['index']
100
106
  manage_job_action = component_dict['action']
101
- daemon = Daemon(daemon_id=daemon_id)
107
+ try:
108
+ daemon = Daemon(daemon_id=daemon_id)
109
+ except Exception as e:
110
+ daemon = None
111
+ if daemon is None:
112
+ raise PreventUpdate
102
113
 
103
114
  manage_functions = {
104
115
  'start': functools.partial(daemon.run, allow_dirty_run=True),
105
116
  'stop': daemon.quit,
106
117
  'pause': daemon.pause,
118
+ 'delete': daemon.cleanup,
107
119
  }
108
120
  if manage_job_action not in manage_functions:
109
121
  return (
@@ -135,6 +147,46 @@ def manage_job_button_click(
135
147
  build_process_timestamps_children(daemon),
136
148
  )
137
149
 
150
+ dash_app.clientside_callback(
151
+ """
152
+ function(n_clicks_arr, url){
153
+ display_block = {"display": "block"};
154
+
155
+ var clicked = false;
156
+ for (var i = 0; i < n_clicks_arr.length; i++){
157
+ if (n_clicks_arr[i]){
158
+ clicked = true;
159
+ break;
160
+ }
161
+ }
162
+
163
+ if (!clicked){
164
+ return dash_clientside.no_update;
165
+ }
166
+
167
+ const triggered_id = dash_clientside.callback_context.triggered_id;
168
+ const job_daemon_id = triggered_id["index"];
169
+
170
+ iframe = document.getElementById('webterm-iframe');
171
+ if (!iframe){ return dash_clientside.no_update; }
172
+
173
+ iframe.contentWindow.postMessage(
174
+ {
175
+ action: "show",
176
+ subaction: "logs",
177
+ subaction_text: job_daemon_id,
178
+ },
179
+ url
180
+ );
181
+ dash_clientside.set_props("webterm-div", {"style": display_block});
182
+ return [];
183
+ }
184
+ """,
185
+ Output('content-div-right', 'children'),
186
+ Input({'type': 'follow-logs-button', 'index': ALL}, 'n_clicks'),
187
+ State('location', 'href'),
188
+ )
189
+
138
190
 
139
191
  @dash_app.callback(
140
192
  Output({'type': 'manage-job-buttons-div', 'index': ALL}, 'children'),
@@ -49,6 +49,17 @@ def get_jobs_cards(state: WebState):
49
49
  build_process_timestamps_children(d),
50
50
  id = {'type': 'process-timestamps-div', 'index': d.daemon_id},
51
51
  )
52
+ follow_logs_button = dbc.DropdownMenuItem(
53
+ "Follow logs",
54
+ id = {'type': 'follow-logs-button', 'index': d.daemon_id},
55
+ )
56
+ download_logs_button = dbc.DropdownMenuItem(
57
+ "Download logs",
58
+ id = {'type': 'job-download-logs-button', 'index': d.daemon_id},
59
+ )
60
+ logs_menu_children = (
61
+ ([follow_logs_button] if is_authenticated else []) + [download_logs_button]
62
+ )
52
63
  header_children = [
53
64
  html.Div(
54
65
  build_status_children(d),
@@ -56,11 +67,13 @@ def get_jobs_cards(state: WebState):
56
67
  style = {'float': 'left'},
57
68
  ),
58
69
  html.Div(
59
- dbc.Button(
60
- 'Download logs',
61
- size = 'sm',
62
- color = 'link',
63
- id = {'type': 'job-download-logs-button', 'index': d.daemon_id},
70
+ dbc.DropdownMenu(
71
+ logs_menu_children,
72
+ label = "Logs",
73
+ size = "sm",
74
+ align_end = True,
75
+ color = "secondary",
76
+ menu_variant = 'dark',
64
77
  ),
65
78
  style = {'float': 'right'},
66
79
  ),
@@ -74,7 +87,7 @@ def get_jobs_cards(state: WebState):
74
87
  className = "card-text job-card-text",
75
88
  style = {"word-wrap": "break-word"}
76
89
  ),
77
- style={"white-space": "pre-wrap"},
90
+ style = {"white-space": "pre-wrap"},
78
91
  ),
79
92
  html.Div(
80
93
  (
@@ -82,7 +95,7 @@ def get_jobs_cards(state: WebState):
82
95
  if is_authenticated
83
96
  else []
84
97
  ),
85
- id={'type': 'manage-job-buttons-div', 'index': d.daemon_id}
98
+ id = {'type': 'manage-job-buttons-div', 'index': d.daemon_id},
86
99
  ),
87
100
  html.Div(id={'type': 'manage-job-alert-div', 'index': d.daemon_id}),
88
101
  ]
@@ -108,7 +121,7 @@ def build_manage_job_buttons_div_children(daemon: Daemon):
108
121
  return [
109
122
  html.Br(),
110
123
  dbc.Row([
111
- dbc.Col(button, width=4)
124
+ dbc.Col(button, width=6)
112
125
  for button in buttons
113
126
  ])
114
127
  ]
@@ -153,9 +166,22 @@ def build_manage_job_buttons(daemon: Daemon):
153
166
  'index': daemon.daemon_id,
154
167
  },
155
168
  )
169
+ delete_button = dbc.Button(
170
+ 'Delete',
171
+ size = 'sm',
172
+ color = 'danger',
173
+ style = {'width': '100%'},
174
+ id = {
175
+ 'type': 'manage-job-button',
176
+ 'action': 'delete',
177
+ 'index': daemon.daemon_id,
178
+ },
179
+ )
156
180
  buttons = []
157
181
  if daemon.status in ('stopped', 'paused'):
158
182
  buttons.append(start_button)
183
+ if daemon.status == 'stopped':
184
+ buttons.append(delete_button)
159
185
  if daemon.status in ('running',):
160
186
  buttons.append(pause_button)
161
187
  if daemon.status in ('running', 'paused'):
@@ -108,7 +108,7 @@ action_dropdown_row = html.Div(
108
108
  id = 'flags-dropdown',
109
109
  multi = True,
110
110
  placeholder = 'Boolean flags',
111
- options = [],
111
+ options = ['--yes'],
112
112
  value = ['--yes'],
113
113
  ),
114
114
  id = 'flags-dropdown-div',
@@ -11,12 +11,22 @@ from meerschaum.config import __doc__ as doc, get_config
11
11
  from meerschaum.utils.misc import get_connector_labels
12
12
  from meerschaum.utils.packages import attempt_import, import_html, import_dcc, import_pandas
13
13
  from meerschaum.api import endpoints, CHECK_UPDATE
14
- dex = attempt_import('dash_extensions', lazy=False, check_update=CHECK_UPDATE)
15
- dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
14
+ (
15
+ dex,
16
+ px,
17
+ daq,
18
+ dbc,
19
+ ) = attempt_import(
20
+ 'dash_extensions',
21
+ 'plotly.express',
22
+ 'dash_daq',
23
+ 'dash_bootstrap_components',
24
+ lazy = False,
25
+ warn = False,
26
+ check_update = CHECK_UPDATE,
27
+ )
16
28
  html, dcc = import_html(check_update=CHECK_UPDATE), import_dcc(check_update=CHECK_UPDATE)
17
29
  pd = import_pandas(check_update=CHECK_UPDATE)
18
- px = attempt_import('plotly.express', warn=False, check_update=CHECK_UPDATE)
19
- daq = attempt_import('dash_daq', warn=False, check_update=CHECK_UPDATE)
20
30
 
21
31
  from meerschaum.api.dash.components import (
22
32
  go_button,
@@ -24,6 +24,7 @@ from meerschaum.api.dash import (
24
24
  from meerschaum.api.dash.connectors import get_web_connector
25
25
  from meerschaum.api.dash.components import alert_from_success_tuple
26
26
  from meerschaum.api.dash.users import is_session_authenticated
27
+ from meerschaum.config import get_config
27
28
  import meerschaum as mrsm
28
29
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
29
30
  dash_ace = attempt_import('dash_ace', lazy=False, check_update=CHECK_UPDATE)
@@ -110,43 +111,127 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
110
111
  session_id = (session_data or {}).get('session-id', None)
111
112
  authenticated = is_session_authenticated(str(session_id))
112
113
 
113
- _pipes = pipes_from_state(*keys, as_list=True)
114
- alerts = [alert_from_success_tuple(_pipes)]
115
- if not isinstance(_pipes, list):
116
- _pipes = []
117
- for p in _pipes:
118
- footer_children = dbc.Row([
119
- dbc.Col(
120
- dbc.Button(
121
- 'Download recent data',
122
- size = 'sm',
123
- color = 'link',
124
- id = {'type': 'pipe-download-csv-button', 'index': json.dumps(p.meta)},
125
- )
126
- ),
127
- ])
114
+ pipes = pipes_from_state(*keys, as_list=True)
115
+ alerts = [alert_from_success_tuple(pipes)]
116
+ if not isinstance(pipes, list):
117
+ pipes = []
118
+
119
+ max_num_pipes_cards = get_config('dash', 'max_num_pipes_cards')
120
+ overflow_pipes = pipes[max_num_pipes_cards:]
121
+
122
+ for pipe in pipes[:max_num_pipes_cards]:
123
+ meta_str = json.dumps(pipe.meta)
124
+ footer_children = dbc.Row(
125
+ [
126
+ dbc.Col(
127
+ (
128
+ dbc.DropdownMenu(
129
+ label = "Manage",
130
+ children = [
131
+ dbc.DropdownMenuItem(
132
+ 'Delete',
133
+ id = {
134
+ 'type': 'manage-pipe-button',
135
+ 'index': meta_str,
136
+ 'action': 'delete',
137
+ },
138
+ ),
139
+ dbc.DropdownMenuItem(
140
+ 'Drop',
141
+ id = {
142
+ 'type': 'manage-pipe-button',
143
+ 'index': meta_str,
144
+ 'action': 'drop',
145
+ },
146
+ ),
147
+ dbc.DropdownMenuItem(
148
+ 'Clear',
149
+ id = {
150
+ 'type': 'manage-pipe-button',
151
+ 'index': meta_str,
152
+ 'action': 'clear',
153
+ },
154
+ ),
155
+ dbc.DropdownMenuItem(
156
+ 'Verify',
157
+ id = {
158
+ 'type': 'manage-pipe-button',
159
+ 'index': meta_str,
160
+ 'action': 'verify',
161
+ },
162
+ ),
163
+ dbc.DropdownMenuItem(
164
+ 'Sync',
165
+ id = {
166
+ 'type': 'manage-pipe-button',
167
+ 'index': meta_str,
168
+ 'action': 'sync',
169
+ },
170
+ ),
171
+ ],
172
+ direction = "up",
173
+ menu_variant = "dark",
174
+ size = 'sm',
175
+ color = 'secondary',
176
+ )
177
+ ) if authenticated else [],
178
+ width = 2,
179
+ ),
180
+ dbc.Col(width=6),
181
+ dbc.Col(
182
+ dbc.Button(
183
+ 'Download CSV',
184
+ size = 'sm',
185
+ color = 'link',
186
+ style = {'float': 'right'},
187
+ id = {'type': 'pipe-download-csv-button', 'index': meta_str},
188
+ ),
189
+ width = 4,
190
+ ),
191
+ ],
192
+ justify = 'start',
193
+ )
128
194
  card_body_children = [
129
195
  html.H5(
130
- html.B(str(p)),
196
+ html.B(str(pipe)),
131
197
  className = 'card-title',
132
198
  style = {'font-family': ['monospace']}
133
199
  ),
134
200
  html.Div(
135
201
  dbc.Accordion(
136
- accordion_items_from_pipe(p, authenticated=authenticated),
202
+ accordion_items_from_pipe(pipe, authenticated=authenticated),
137
203
  flush = True,
138
204
  start_collapsed = True,
139
- id = {'type': 'pipe-accordion', 'index': json.dumps(p.meta)},
205
+ id = {'type': 'pipe-accordion', 'index': meta_str},
140
206
  )
141
207
  )
142
208
 
143
209
  ]
144
210
  cards.append(
145
- dbc.Card(children=[
211
+ dbc.Card([
146
212
  dbc.CardBody(children=card_body_children),
147
213
  dbc.CardFooter(children=footer_children),
148
214
  ])
149
215
  )
216
+
217
+ if overflow_pipes:
218
+ cards.append(
219
+ dbc.Card([
220
+ dbc.CardBody(
221
+ html.Ul(
222
+ [
223
+ html.Li(html.H5(
224
+ html.B(str(pipe)),
225
+ className = 'card-title',
226
+ style = {'font-family': ['monospace']}
227
+ ))
228
+ for pipe in overflow_pipes
229
+ ]
230
+ )
231
+ )
232
+ ])
233
+ )
234
+
150
235
  return cards, alerts
151
236
 
152
237
 
@@ -188,14 +273,40 @@ def accordion_items_from_pipe(
188
273
  overview_header = [html.Thead(html.Tr([html.Th("Attribute"), html.Th("Value")]))]
189
274
  dt_name, id_name, val_name = pipe.get_columns('datetime', 'id', 'value', error=False)
190
275
  overview_rows = [
191
- html.Tr([html.Td("Connector"), html.Td(f"{pipe.connector_keys}")]),
192
- html.Tr([html.Td("Metric"), html.Td(f"{pipe.metric_key}")]),
193
- html.Tr([html.Td("Location"), html.Td(f"{pipe.location_key}")]),
194
- html.Tr([html.Td("Instance"), html.Td(f"{pipe.instance_keys}")]),
195
- html.Tr([html.Td("Target Table"), html.Td(f"{pipe.target}")]),
276
+ html.Tr([html.Td("Connector"), html.Td(html.Pre(f"{pipe.connector_keys}"))]),
277
+ html.Tr([html.Td("Metric"), html.Td(html.Pre(f"{pipe.metric_key}"))]),
278
+ html.Tr([html.Td("Location"), html.Td(html.Pre(f"{pipe.location_key}"))]),
279
+ html.Tr([html.Td("Instance"), html.Td(html.Pre(f"{pipe.instance_keys}"))]),
280
+ html.Tr([html.Td("Target Table"), html.Td(html.Pre(f"{pipe.target}"))]),
196
281
  ]
197
- for col_key, col in pipe.columns.items():
198
- overview_rows.append(html.Tr([html.Td(f"'{col_key}' Index"), html.Td(col)]))
282
+ columns = pipe.columns.copy()
283
+ if columns:
284
+ datetime_index = columns.pop('datetime', None)
285
+ columns_items = []
286
+ if datetime_index:
287
+ columns_items.append(html.Li(f"{datetime_index} (datetime)"))
288
+ columns_items.extend([
289
+ html.Li(f"{col}")
290
+ for col_key, col in columns.items()
291
+ ])
292
+ overview_rows.append(
293
+ html.Tr([
294
+ html.Td("Indices" if len(columns_items) != 1 else "Index"),
295
+ html.Td(html.Pre(html.Ul(columns_items))),
296
+ ])
297
+ )
298
+ tags = pipe.tags
299
+ if tags:
300
+ tags_items = html.Ul([
301
+ html.Li(tag)
302
+ for tag in tags
303
+ ])
304
+ overview_rows.append(
305
+ html.Tr([
306
+ html.Td("Tags"),
307
+ html.Td(html.Pre(tags_items)),
308
+ ])
309
+ )
199
310
 
200
311
  items_bodies['overview'] = dbc.Table(
201
312
  overview_header + [html.Tbody(overview_rows)],
@@ -32,17 +32,22 @@ def get_plugins_cards(
32
32
  desc = get_api_connector().get_plugin_attributes(plugin).get(
33
33
  'description', 'No description provided.'
34
34
  )
35
- desc_textarea_kw = dict(
36
- value=desc, readOnly=True, debounce=False, className='plugin-description',
37
- draggable=False, wrap='overflow',
38
- id={'type': 'description-textarea', 'index': plugin_name},
39
- )
35
+ desc_textarea_kw = {
36
+ 'value': desc,
37
+ 'readOnly': True,
38
+ 'debounce': False,
39
+ 'className': 'plugin-description',
40
+ 'draggable': False,
41
+ 'wrap': 'overflow',
42
+ 'placeholder': "Edit the plugin's description",
43
+ 'id': {'type': 'description-textarea', 'index': plugin_name},
44
+ }
40
45
 
41
46
  card_body_children = [html.H4(plugin_name)]
42
47
 
43
48
  if is_plugin_owner(plugin_name, session_data):
44
49
  desc_textarea_kw['readOnly'] = False
45
- card_body_children += [dbc.Textarea(**desc_textarea_kw)]
50
+ card_body_children.append(dbc.Textarea(**desc_textarea_kw))
46
51
  if not desc_textarea_kw['readOnly']:
47
52
  card_body_children += [
48
53
  dbc.Button(
@@ -53,12 +58,23 @@ def get_plugins_cards(
53
58
  ),
54
59
  html.Div(id={'type': 'edit-alert-div', 'index': plugin_name}),
55
60
  ]
56
- _plugin_username = get_api_connector().get_plugin_username(plugin, debug=debug)
61
+ plugin_username = get_api_connector().get_plugin_username(plugin, debug=debug)
62
+ plugin_version = get_api_connector().get_plugin_version(plugin, debug=debug) or ' '
57
63
  card_children = [
58
- dbc.CardHeader([html.A('👤 ' + str(_plugin_username), href='#')]),
64
+ dbc.CardHeader(
65
+ [
66
+ dbc.Row(
67
+ [
68
+ dbc.Col(html.A('👤 ' + str(plugin_username), href='#')),
69
+ dbc.Col(html.Pre(str(plugin_version), style={'text-align': 'right'})),
70
+ ],
71
+ justify = 'between',
72
+ ),
73
+ ],
74
+ ),
59
75
  dbc.CardBody(card_body_children),
60
76
  dbc.CardFooter([
61
- html.A('⬇️ Download source', href=(endpoints['plugins'] + '/' + plugin_name))
77
+ html.A('⬇️ Download', href=(endpoints['plugins'] + '/' + plugin_name))
62
78
  ]),
63
79
  ]
64
80
  cards.append(