meerschaum 2.2.6__py3-none-any.whl → 2.3.0.dev1__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 (61) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/__main__.py +10 -5
  3. meerschaum/_internal/arguments/_parser.py +44 -15
  4. meerschaum/_internal/entry.py +35 -14
  5. meerschaum/_internal/shell/Shell.py +155 -53
  6. meerschaum/_internal/shell/updates.py +175 -0
  7. meerschaum/actions/api.py +12 -12
  8. meerschaum/actions/attach.py +95 -0
  9. meerschaum/actions/delete.py +35 -26
  10. meerschaum/actions/register.py +19 -5
  11. meerschaum/actions/show.py +119 -148
  12. meerschaum/actions/start.py +85 -75
  13. meerschaum/actions/stop.py +68 -39
  14. meerschaum/actions/sync.py +3 -3
  15. meerschaum/actions/upgrade.py +28 -36
  16. meerschaum/api/_events.py +18 -1
  17. meerschaum/api/_oauth2.py +2 -0
  18. meerschaum/api/_websockets.py +2 -2
  19. meerschaum/api/dash/jobs.py +5 -2
  20. meerschaum/api/routes/__init__.py +1 -0
  21. meerschaum/api/routes/_actions.py +122 -44
  22. meerschaum/api/routes/_jobs.py +340 -0
  23. meerschaum/api/routes/_pipes.py +25 -25
  24. meerschaum/config/_default.py +1 -0
  25. meerschaum/config/_formatting.py +1 -0
  26. meerschaum/config/_paths.py +5 -0
  27. meerschaum/config/_shell.py +84 -67
  28. meerschaum/config/_version.py +1 -1
  29. meerschaum/config/static/__init__.py +9 -0
  30. meerschaum/connectors/__init__.py +9 -11
  31. meerschaum/connectors/api/APIConnector.py +18 -1
  32. meerschaum/connectors/api/_actions.py +60 -71
  33. meerschaum/connectors/api/_jobs.py +260 -0
  34. meerschaum/connectors/api/_misc.py +1 -1
  35. meerschaum/connectors/api/_request.py +13 -9
  36. meerschaum/connectors/parse.py +23 -7
  37. meerschaum/core/Pipe/_sync.py +3 -0
  38. meerschaum/plugins/__init__.py +89 -5
  39. meerschaum/utils/daemon/Daemon.py +333 -149
  40. meerschaum/utils/daemon/FileDescriptorInterceptor.py +19 -10
  41. meerschaum/utils/daemon/RotatingFile.py +18 -7
  42. meerschaum/utils/daemon/StdinFile.py +110 -0
  43. meerschaum/utils/daemon/__init__.py +40 -27
  44. meerschaum/utils/formatting/__init__.py +83 -37
  45. meerschaum/utils/formatting/_jobs.py +118 -51
  46. meerschaum/utils/formatting/_shell.py +6 -0
  47. meerschaum/utils/jobs/_Job.py +684 -0
  48. meerschaum/utils/jobs/__init__.py +245 -0
  49. meerschaum/utils/misc.py +18 -17
  50. meerschaum/utils/packages/__init__.py +21 -15
  51. meerschaum/utils/packages/_packages.py +2 -2
  52. meerschaum/utils/prompt.py +20 -7
  53. meerschaum/utils/schedule.py +21 -15
  54. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/METADATA +9 -9
  55. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/RECORD +61 -54
  56. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/WHEEL +1 -1
  57. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/LICENSE +0 -0
  58. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/NOTICE +0 -0
  59. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/entry_points.txt +0 -0
  60. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/top_level.txt +0 -0
  61. {meerschaum-2.2.6.dist-info → meerschaum-2.3.0.dev1.dist-info}/zip-safe +0 -0
@@ -0,0 +1,340 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Manage jobs via the Meerschaum API.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import select
13
+ import asyncio
14
+ import traceback
15
+ from datetime import datetime
16
+ from collections import defaultdict
17
+ from functools import partial
18
+
19
+ from fastapi import WebSocket, WebSocketDisconnect
20
+
21
+ from meerschaum.utils.typing import Dict, Any, SuccessTuple, List, Optional, Union
22
+ from meerschaum.utils.jobs import get_jobs as _get_jobs, Job, StopMonitoringLogs
23
+ from meerschaum.utils.warnings import warn
24
+
25
+ from meerschaum.api import (
26
+ fastapi,
27
+ app,
28
+ endpoints,
29
+ manager,
30
+ debug,
31
+ no_auth,
32
+ private,
33
+ )
34
+ from meerschaum.config.static import STATIC_CONFIG
35
+
36
+ JOBS_STDIN_MESSAGE: str = STATIC_CONFIG['api']['jobs']['stdin_message']
37
+
38
+
39
+ @app.get(endpoints['jobs'], tags=['Jobs'])
40
+ def get_jobs(
41
+ curr_user=(
42
+ fastapi.Depends(manager) if not no_auth else None
43
+ ),
44
+ ) -> Dict[str, Dict[str, Any]]:
45
+ """
46
+ Return metadata about the current jobs.
47
+ """
48
+ jobs = _get_jobs()
49
+ return {
50
+ name: {
51
+ 'sysargs': job.sysargs,
52
+ 'result': job.result,
53
+ 'daemon': {
54
+ 'status': job.daemon.status,
55
+ 'pid': job.daemon.pid,
56
+ 'properties': job.daemon.properties,
57
+ },
58
+ }
59
+ for name, job in jobs.items()
60
+ }
61
+
62
+
63
+ @app.get(endpoints['jobs'] + '/{name}', tags=['Jobs'])
64
+ def get_job(
65
+ name: str,
66
+ curr_user=(
67
+ fastapi.Depends(manager) if not no_auth else None
68
+ ),
69
+ ) -> Dict[str, Any]:
70
+ """
71
+ Return metadata for a single job.
72
+ """
73
+ job = Job(name)
74
+ if not job.exists():
75
+ raise fastapi.HTTPException(
76
+ status_code=404,
77
+ detail=f"{job} doesn't exist."
78
+ )
79
+
80
+ return {
81
+ 'sysargs': job.sysargs,
82
+ 'result': job.result,
83
+ 'daemon': {
84
+ 'status': job.daemon.status,
85
+ 'pid': job.daemon.pid,
86
+ 'properties': job.daemon.properties,
87
+ },
88
+ }
89
+
90
+
91
+ @app.post(endpoints['jobs'] + '/{name}', tags=['Jobs'])
92
+ def create_job(
93
+ name: str,
94
+ sysargs: List[str],
95
+ curr_user=(
96
+ fastapi.Depends(manager) if not no_auth else None
97
+ ),
98
+ ) -> SuccessTuple:
99
+ """
100
+ Create and start a new job.
101
+ """
102
+ job = Job(name, sysargs)
103
+ if job.exists():
104
+ raise fastapi.HTTPException(
105
+ status_code=409,
106
+ detail=f"{job} already exists."
107
+ )
108
+
109
+ return job.start()
110
+
111
+
112
+ @app.delete(endpoints['jobs'] + '/{name}', tags=['Jobs'])
113
+ def delete_job(
114
+ name: str,
115
+ curr_user=(
116
+ fastapi.Depends(manager) if not no_auth else None
117
+ ),
118
+ ) -> SuccessTuple:
119
+ """
120
+ Delete a job.
121
+ """
122
+ job = Job(name)
123
+ return job.delete()
124
+
125
+
126
+ @app.get(endpoints['jobs'] + '/{name}/exists', tags=['Jobs'])
127
+ def get_job_exists(
128
+ name: str,
129
+ curr_user=(
130
+ fastapi.Depends(manager) if not no_auth else None
131
+ ),
132
+ ) -> bool:
133
+ """
134
+ Return whether a job exists.
135
+ """
136
+ job = Job(name)
137
+ return job.exists()
138
+
139
+
140
+ @app.get(endpoints['logs'] + '/{name}', tags=['Jobs'])
141
+ def get_logs(
142
+ name: str,
143
+ curr_user=(
144
+ fastapi.Depends(manager) if not no_auth else None
145
+ ),
146
+ ) -> Union[str, None]:
147
+ """
148
+ Return a job's log text.
149
+ To stream log text, connect to the WebSocket endpoint `/logs/{name}/ws`.
150
+ """
151
+ job = Job(name)
152
+ if not job.exists():
153
+ raise fastapi.HTTPException(
154
+ status_code=404,
155
+ detail=f"{job} does not exist.",
156
+ )
157
+
158
+ return job.get_logs()
159
+
160
+
161
+ @app.post(endpoints['jobs'] + '/{name}/start', tags=['Jobs'])
162
+ def start_job(
163
+ name: str,
164
+ curr_user=(
165
+ fastapi.Depends(manager) if not no_auth else None
166
+ ),
167
+ ) -> SuccessTuple:
168
+ """
169
+ Start a job if stopped.
170
+ """
171
+ job = Job(name)
172
+ if not job.exists():
173
+ raise fastapi.HTTPException(
174
+ status_code=404,
175
+ detail=f"{job} does not exist."
176
+ )
177
+ return job.start()
178
+
179
+
180
+ @app.post(endpoints['jobs'] + '/{name}/stop', tags=['Jobs'])
181
+ def stop_job(
182
+ name: str,
183
+ curr_user=(
184
+ fastapi.Depends(manager) if not no_auth else None
185
+ ),
186
+ ) -> SuccessTuple:
187
+ """
188
+ Stop a job if running.
189
+ """
190
+ job = Job(name)
191
+ if not job.exists():
192
+ raise fastapi.HTTPException(
193
+ status_code=404,
194
+ detail=f"{job} does not exist."
195
+ )
196
+ return job.stop()
197
+
198
+
199
+ @app.post(endpoints['jobs'] + '/{name}/pause', tags=['Jobs'])
200
+ def pause_job(
201
+ name: str,
202
+ curr_user=(
203
+ fastapi.Depends(manager) if not no_auth else None
204
+ ),
205
+ ) -> SuccessTuple:
206
+ """
207
+ Pause a job if running.
208
+ """
209
+ job = Job(name)
210
+ if not job.exists():
211
+ raise fastapi.HTTPException(
212
+ status_code=404,
213
+ detail=f"{job} does not exist."
214
+ )
215
+ return job.pause()
216
+
217
+
218
+ @app.get(endpoints['jobs'] + '/{name}/stop_time', tags=['Jobs'])
219
+ def get_stop_time(
220
+ name: str,
221
+ curr_user=(
222
+ fastapi.Depends(manager) if not no_auth else None
223
+ ),
224
+ ) -> Union[datetime, None]:
225
+ """
226
+ Get the timestamp when the job was manually stopped.
227
+ """
228
+ job = Job(name)
229
+ return job.stop_time
230
+
231
+
232
+ @app.get(endpoints['jobs'] + '/{name}/is_blocking_on_stdin', tags=['Jobs'])
233
+ def get_is_blocking_on_stdin(
234
+ name: str,
235
+ curr_user=(
236
+ fastapi.Depends(manager) if not no_auth else None
237
+ ),
238
+ ) -> bool:
239
+ """
240
+ Return whether a job is blocking on stdin.
241
+ """
242
+ job = Job(name)
243
+ return job.is_blocking_on_stdin()
244
+
245
+
246
+ _job_clients = defaultdict(lambda: [])
247
+ _job_stop_events = defaultdict(lambda: asyncio.Event())
248
+ async def notify_clients(name: str, content: str):
249
+ """
250
+ Write the given content to all connected clients.
251
+ """
252
+ if not _job_clients[name]:
253
+ _job_stop_events[name].set()
254
+
255
+ async def _notify_client(client):
256
+ try:
257
+ await client.send_text(content)
258
+ except WebSocketDisconnect:
259
+ if client in _job_clients[name]:
260
+ _job_clients[name].remove(client)
261
+ except Exception:
262
+ pass
263
+
264
+ notify_tasks = [
265
+ asyncio.create_task(_notify_client(client))
266
+ for client in _job_clients[name]
267
+ ]
268
+ await asyncio.wait(notify_tasks)
269
+
270
+
271
+ async def get_input_from_clients(name):
272
+ """
273
+ When a job is blocking on input, return input from the first client which provides it.
274
+ """
275
+ print('GET INPUT FROM CLIENTS')
276
+ if not _job_clients[name]:
277
+ print('NO CLIENTS')
278
+ return ''
279
+
280
+ async def _read_client(client):
281
+ try:
282
+ await client.send_text(JOBS_STDIN_MESSAGE)
283
+ data = await client.receive_text()
284
+ except WebSocketDisconnect:
285
+ if client in _job_clients[name]:
286
+ _job_clients[name].remove(client)
287
+ except Exception:
288
+ pass
289
+ return data
290
+
291
+ read_tasks = [
292
+ asyncio.create_task(_read_client(client))
293
+ for client in _job_clients[name]
294
+ ]
295
+ done, pending = await asyncio.wait(read_tasks, return_when=asyncio.FIRST_COMPLETED)
296
+ for task in pending:
297
+ task.cancel()
298
+ for task in done:
299
+ return task.result()
300
+
301
+
302
+ @app.websocket(endpoints['logs'] + '/{name}/ws')
303
+ async def logs_websocket(name: str, websocket: WebSocket):
304
+ """
305
+ Stream logs from a job over a websocket.
306
+ """
307
+ await websocket.accept()
308
+ job = Job(name)
309
+ _job_clients[name].append(websocket)
310
+
311
+ async def monitor_logs():
312
+ await job.monitor_logs_async(
313
+ partial(notify_clients, name),
314
+ input_callback_function=partial(get_input_from_clients, name),
315
+ stop_event=_job_stop_events[name],
316
+ )
317
+
318
+ try:
319
+ token = await websocket.receive_text()
320
+ user = await manager.get_current_user(token) if not no_auth else None
321
+ if user is None and not no_auth:
322
+ raise fastapi.HTTPException(
323
+ status_code=401,
324
+ detail="Invalid credentials.",
325
+ )
326
+ monitor_task = asyncio.create_task(monitor_logs())
327
+ await monitor_task
328
+ except fastapi.HTTPException:
329
+ await websocket.send_text("Invalid credentials.")
330
+ await websocket.close()
331
+ except WebSocketDisconnect:
332
+ pass
333
+ except asyncio.CancelledError:
334
+ pass
335
+ except Exception:
336
+ warn(f"Error in logs websocket:\n{traceback.format_exc()}")
337
+ finally:
338
+ if websocket in _job_clients[name]:
339
+ _job_clients[name].remove(websocket)
340
+ _job_stop_events[name].clear()
@@ -44,14 +44,14 @@ pd = attempt_import('pandas')
44
44
 
45
45
  @app.post(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/register', tags=['Pipes'])
46
46
  def register_pipe(
47
- connector_keys: str,
48
- metric_key: str,
49
- location_key: str,
50
- parameters: dict,
51
- curr_user = (
52
- fastapi.Depends(manager) if not no_auth else None
53
- ),
54
- ):
47
+ connector_keys: str,
48
+ metric_key: str,
49
+ location_key: str,
50
+ parameters: dict,
51
+ curr_user = (
52
+ fastapi.Depends(manager) if not no_auth else None
53
+ ),
54
+ ):
55
55
  """
56
56
  Register a new pipe.
57
57
  """
@@ -201,13 +201,13 @@ async def fetch_pipes_keys(
201
201
 
202
202
  @app.get(pipes_endpoint, tags=['Pipes'])
203
203
  async def get_pipes(
204
- connector_keys : str = "",
205
- metric_keys : str = "",
206
- location_keys : str = "",
207
- curr_user = (
204
+ connector_keys: str = "",
205
+ metric_keys: str = "",
206
+ location_keys: str = "",
207
+ curr_user=(
208
208
  fastapi.Depends(manager) if not no_auth else None
209
209
  ),
210
- debug : bool = False
210
+ debug: bool = False,
211
211
  ) -> Dict[str, Any]:
212
212
  """
213
213
  Get all registered Pipes with metadata, excluding parameters.
@@ -376,18 +376,18 @@ def sync_pipe(
376
376
 
377
377
  @app.get(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/data', tags=['Pipes'])
378
378
  def get_pipe_data(
379
- connector_keys: str,
380
- metric_key: str,
381
- location_key: str,
382
- select_columns: Optional[str] = None,
383
- omit_columns: Optional[str] = None,
384
- begin: Union[str, int, None] = None,
385
- end: Union[str, int, None] = None,
386
- params: Optional[str] = None,
387
- curr_user = (
388
- fastapi.Depends(manager) if not no_auth else None
389
- ),
390
- ) -> str:
379
+ connector_keys: str,
380
+ metric_key: str,
381
+ location_key: str,
382
+ select_columns: Optional[str] = None,
383
+ omit_columns: Optional[str] = None,
384
+ begin: Union[str, int, None] = None,
385
+ end: Union[str, int, None] = None,
386
+ params: Optional[str] = None,
387
+ curr_user = (
388
+ fastapi.Depends(manager) if not no_auth else None
389
+ ),
390
+ ) -> str:
391
391
  """
392
392
  Get a pipe's data, applying any filtering.
393
393
 
@@ -16,6 +16,7 @@ default_meerschaum_config = {
16
16
  'api_instance': 'MRSM{meerschaum:instance}',
17
17
  'web_instance': 'MRSM{meerschaum:instance}',
18
18
  'default_repository': 'api:mrsm',
19
+ 'default_executor': 'local',
19
20
  'connectors': {
20
21
  'sql': {
21
22
  'default': {},
@@ -36,6 +36,7 @@ default_formatting_config = {
36
36
  'paused' : '🟡',
37
37
  'stopped' : '🔴',
38
38
  'tag' : '🔖',
39
+ 'announcement' : '📢',
39
40
  },
40
41
  'pipes' : {
41
42
  'unicode' : {
@@ -123,6 +123,10 @@ paths = {
123
123
  'PERMANENT_PATCH_DIR_PATH' : ('{ROOT_DIR_PATH}', 'permanent_patch_config'),
124
124
  'INTERNAL_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', '.internal'),
125
125
 
126
+ 'UPDATES_RESOURCES_PATH' : ('{INTERNAL_RESOURCES_PATH}', 'updates'),
127
+ 'UPDATES_CACHE_PATH' : ('{UPDATES_RESOURCES_PATH}', 'cache.json'),
128
+ 'UPDATES_LOCK_PATH' : ('{UPDATES_RESOURCES_PATH}', '.updates.lock'),
129
+
126
130
  'STACK_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'stack'),
127
131
  'STACK_COMPOSE_FILENAME' : 'docker-compose.yaml',
128
132
  'STACK_COMPOSE_PATH' : ('{STACK_RESOURCES_PATH}', '{STACK_COMPOSE_FILENAME}'),
@@ -176,6 +180,7 @@ paths = {
176
180
  'DAEMON_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'jobs'),
177
181
  'LOGS_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'logs'),
178
182
  'DAEMON_ERROR_LOG_PATH' : ('{ROOT_DIR_PATH}', 'daemon_errors.log'),
183
+ 'CHECK_JOBS_LOCK_PATH' : ('{INTERNAL_RESOURCES_PATH}', 'check-jobs.lock'),
179
184
  }
180
185
 
181
186
  def set_root(root: Union[Path, str]):
@@ -6,127 +6,144 @@
6
6
  Default configuration for the Meerschaum shell.
7
7
  """
8
8
 
9
- # import platform
10
- # default_cmd = 'cmd' if platform.system() != 'Windows' else 'cmd2'
11
9
  default_cmd = 'cmd'
12
10
 
13
11
  default_shell_config = {
14
- 'ansi' : {
15
- 'intro' : {
16
- 'rich' : {
17
- 'style' : "bold bright_blue",
12
+ 'ansi' : {
13
+ 'intro' : {
14
+ 'rich' : {
15
+ 'style' : "bold bright_blue",
18
16
  },
19
- 'color' : [
17
+ 'color' : [
20
18
  'bold',
21
19
  'bright blue',
22
20
  ],
23
21
  },
24
- 'close_message': {
25
- 'rich' : {
26
- 'style' : 'bright_blue',
22
+ 'close_message' : {
23
+ 'rich' : {
24
+ 'style' : 'bright_blue',
27
25
  },
28
- 'color' : [
26
+ 'color' : [
29
27
  'bright blue',
30
28
  ],
31
29
  },
32
- 'doc_header': {
33
- 'rich' : {
34
- 'style' : 'bright_blue',
30
+ 'doc_header' : {
31
+ 'rich' : {
32
+ 'style' : 'bright_blue',
35
33
  },
36
- 'color' : [
34
+ 'color' : [
37
35
  'bright blue',
38
36
  ],
39
37
  },
40
- 'undoc_header': {
41
- 'rich' : {
42
- 'style' : 'bright_blue',
38
+ 'undoc_header' : {
39
+ 'rich' : {
40
+ 'style' : 'bright_blue',
43
41
  },
44
- 'color' : [
42
+ 'color' : [
45
43
  'bright blue',
46
44
  ],
47
45
  },
48
- 'ruler': {
49
- 'rich' : {
50
- 'style' : 'bold bright_blue',
46
+ 'ruler' : {
47
+ 'rich' : {
48
+ 'style' : 'bold bright_blue',
51
49
  },
52
- 'color' : [
50
+ 'color' : [
53
51
  'bold',
54
52
  'bright blue',
55
53
  ],
56
54
  },
57
- 'prompt': {
58
- 'rich' : {
59
- 'style' : 'green',
55
+ 'prompt' : {
56
+ 'rich' : {
57
+ 'style' : 'green',
60
58
  },
61
- 'color' : [
59
+ 'color' : [
62
60
  'green',
63
61
  ],
64
62
  },
65
- 'instance' : {
66
- 'rich' : {
67
- 'style' : 'cyan',
63
+ 'instance' : {
64
+ 'rich' : {
65
+ 'style' : 'cyan',
68
66
  },
69
- 'color' : [
67
+ 'color' : [
70
68
  'cyan',
71
69
  ],
72
70
  },
73
- 'repo' : {
74
- 'rich': {
75
- 'style': 'magenta',
71
+ 'repo' : {
72
+ 'rich' : {
73
+ 'style' : 'magenta',
76
74
  },
77
- 'color': [
75
+ 'color' : [
78
76
  'magenta',
79
77
  ],
80
78
  },
81
- 'username' : {
82
- 'rich' : {
83
- 'style' : 'white',
79
+ 'executor' : {
80
+ 'rich' : {
81
+ 'style' : 'gold1',
84
82
  },
85
- 'color' : [
83
+ },
84
+ 'username' : {
85
+ 'rich' : {
86
+ 'style' : 'white',
87
+ },
88
+ 'color' : [
86
89
  'white',
87
90
  ],
88
91
  },
89
- 'connected' : {
90
- 'rich' : {
91
- 'style' : 'green',
92
+ 'connected' : {
93
+ 'rich' : {
94
+ 'style' : 'green',
92
95
  },
93
- 'color' : [
96
+ 'color' : [
94
97
  'green',
95
98
  ],
96
99
  },
97
- 'disconnected' : {
98
- 'rich' : {
99
- 'style' : 'red',
100
+ 'disconnected' : {
101
+ 'rich' : {
102
+ 'style' : 'red',
100
103
  },
101
- 'color' : [
104
+ 'color' : [
105
+ 'red',
106
+ ],
107
+ },
108
+ 'update_message' : {
109
+ 'rich' : {
110
+ 'style' : 'red',
111
+ },
112
+ 'color' : [
102
113
  'red',
103
114
  ],
104
115
  },
105
116
  },
106
- 'ascii' : {
107
- 'intro' : """ ___ ___ __ __ __
117
+ 'ascii' : {
118
+ 'intro' : r""" ___ ___ __ __ __
108
119
  |\/| |__ |__ |__) /__` / ` |__| /\ | | |\/|
109
- | | |___ |___ | \ .__/ \__, | | /~~\ \__/ | |\n""",
110
- 'prompt' : '\n [ {username}@{instance} ] > ',
111
- 'ruler' : '-',
112
- 'close_message': 'Thank you for using Meerschaum!',
113
- 'doc_header' : 'Meerschaum actions (`help <action>` for usage):',
114
- 'undoc_header' : 'Unimplemented actions:',
120
+ | | |___ |___ | \ .__/ \__, | | /~~\ \__/ | |""" + '\n',
121
+ 'prompt' : '\n [ {username}@{instance} | {executor_keys} ] > ',
122
+ 'ruler' : '-',
123
+ 'close_message' : 'Thank you for using Meerschaum!',
124
+ 'doc_header' : 'Meerschaum actions (`help <action>` for usage):',
125
+ 'undoc_header' : 'Unimplemented actions:',
126
+ 'update_message' : "Update available!",
115
127
  },
116
- 'unicode' : {
117
- 'intro' : """
128
+ 'unicode' : {
129
+ 'intro' : """
118
130
  █▄ ▄█ ██▀ ██▀ █▀▄ ▄▀▀ ▄▀▀ █▄█ ▄▀▄ █ █ █▄ ▄█
119
131
  █ ▀ █ █▄▄ █▄▄ █▀▄ ▄██ ▀▄▄ █ █ █▀█ ▀▄█ █ ▀ █\n""",
120
- 'prompt' : '\n [ {username}@{instance} ] ➤ ',
121
- 'ruler' : '─',
122
- 'close_message': ' MRSM{formatting:emoji:hand} Thank you for using Meerschaum! ',
123
- 'doc_header' : 'Meerschaum actions (`help <action>` for usage):',
124
- 'undoc_header' : 'Unimplemented actions:',
132
+ 'prompt' : '\n [ {username}@{instance} | {executor_keys} ] ➤ ',
133
+ 'ruler' : '─',
134
+ 'close_message' : ' MRSM{formatting:emoji:hand} Thank you for using Meerschaum! ',
135
+ 'doc_header' : 'Meerschaum actions (`help <action>` for usage):',
136
+ 'undoc_header' : 'Unimplemented actions:',
137
+ 'update_message' : "MRSM{formatting:emoji:announcement} Update available!",
138
+ },
139
+ 'timeout' : 60,
140
+ 'max_history' : 1000,
141
+ 'clear_screen' : True,
142
+ 'bottom_toolbar' : {
143
+ 'enabled' : True,
125
144
  },
126
- 'timeout' : 60,
127
- 'max_history' : 1000,
128
- 'clear_screen' : True,
129
- 'bottom_toolbar' : {
130
- 'enabled' : True,
145
+ 'updates' : {
146
+ 'check_remote' : True,
147
+ 'refresh_minutes': 180,
131
148
  },
132
149
  }
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.2.6"
5
+ __version__ = "2.3.0.dev1"