meerschaum 2.2.7__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 (50) hide show
  1. meerschaum/__init__.py +4 -1
  2. meerschaum/_internal/arguments/_parser.py +44 -15
  3. meerschaum/_internal/entry.py +22 -1
  4. meerschaum/_internal/shell/Shell.py +129 -31
  5. meerschaum/actions/api.py +12 -12
  6. meerschaum/actions/attach.py +95 -0
  7. meerschaum/actions/delete.py +35 -26
  8. meerschaum/actions/show.py +119 -148
  9. meerschaum/actions/start.py +85 -75
  10. meerschaum/actions/stop.py +68 -39
  11. meerschaum/api/_events.py +18 -1
  12. meerschaum/api/_oauth2.py +2 -0
  13. meerschaum/api/_websockets.py +2 -2
  14. meerschaum/api/dash/jobs.py +5 -2
  15. meerschaum/api/routes/__init__.py +1 -0
  16. meerschaum/api/routes/_actions.py +122 -44
  17. meerschaum/api/routes/_jobs.py +340 -0
  18. meerschaum/api/routes/_pipes.py +5 -5
  19. meerschaum/config/_default.py +1 -0
  20. meerschaum/config/_paths.py +1 -0
  21. meerschaum/config/_shell.py +8 -3
  22. meerschaum/config/_version.py +1 -1
  23. meerschaum/config/static/__init__.py +8 -0
  24. meerschaum/connectors/__init__.py +9 -11
  25. meerschaum/connectors/api/APIConnector.py +18 -1
  26. meerschaum/connectors/api/_actions.py +60 -71
  27. meerschaum/connectors/api/_jobs.py +260 -0
  28. meerschaum/connectors/parse.py +23 -7
  29. meerschaum/plugins/__init__.py +89 -5
  30. meerschaum/utils/daemon/Daemon.py +255 -30
  31. meerschaum/utils/daemon/FileDescriptorInterceptor.py +5 -5
  32. meerschaum/utils/daemon/RotatingFile.py +10 -6
  33. meerschaum/utils/daemon/StdinFile.py +110 -0
  34. meerschaum/utils/daemon/__init__.py +13 -7
  35. meerschaum/utils/formatting/__init__.py +2 -1
  36. meerschaum/utils/formatting/_jobs.py +83 -54
  37. meerschaum/utils/formatting/_shell.py +6 -0
  38. meerschaum/utils/jobs/_Job.py +684 -0
  39. meerschaum/utils/jobs/__init__.py +245 -0
  40. meerschaum/utils/misc.py +18 -17
  41. meerschaum/utils/packages/_packages.py +2 -2
  42. meerschaum/utils/prompt.py +16 -8
  43. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/METADATA +9 -9
  44. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/RECORD +50 -44
  45. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/WHEEL +1 -1
  46. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/LICENSE +0 -0
  47. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/NOTICE +0 -0
  48. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/entry_points.txt +0 -0
  49. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/top_level.txt +0 -0
  50. {meerschaum-2.2.7.dist-info → meerschaum-2.3.0.dev1.dist-info}/zip-safe +0 -0
@@ -78,6 +78,9 @@ def _start_api(action: Optional[List[str]] = None, **kw):
78
78
  def _start_jobs(
79
79
  action: Optional[List[str]] = None,
80
80
  name: Optional[str] = None,
81
+ sysargs: Optional[List[str]] = None,
82
+ executor_keys: Optional[str] = None,
83
+ debug: bool = False,
81
84
  **kw
82
85
  ) -> SuccessTuple:
83
86
  """
@@ -111,11 +114,16 @@ def _start_jobs(
111
114
  """
112
115
  import textwrap
113
116
  from meerschaum.utils.warnings import warn, info
114
- from meerschaum.utils.daemon import (
115
- daemon_action, Daemon, get_daemon_ids, get_daemons, get_filtered_daemons,
116
- get_stopped_daemons, get_running_daemons, get_paused_daemons,
117
- )
118
117
  from meerschaum.utils.daemon._names import get_new_daemon_name
118
+ from meerschaum.utils.jobs import (
119
+ Job,
120
+ get_jobs,
121
+ get_filtered_jobs,
122
+ get_stopped_jobs,
123
+ get_running_jobs,
124
+ get_paused_jobs,
125
+ get_restart_jobs,
126
+ )
119
127
  from meerschaum._internal.arguments._parse_arguments import parse_arguments
120
128
  from meerschaum.actions import actions
121
129
  from meerschaum.utils.prompt import yes_no
@@ -125,7 +133,7 @@ def _start_jobs(
125
133
  from meerschaum.utils.misc import items_str
126
134
 
127
135
  names = []
128
- daemon_ids = get_daemon_ids()
136
+ jobs = get_filtered_jobs(executor_keys, debug=debug)
129
137
 
130
138
  new_job = len(list(action)) > 0
131
139
  _potential_jobs = {'known': [], 'unknown': []}
@@ -134,7 +142,7 @@ def _start_jobs(
134
142
  for a in action:
135
143
  _potential_jobs[(
136
144
  'known'
137
- if a in daemon_ids
145
+ if a in jobs
138
146
  else 'unknown'
139
147
  )].append(a)
140
148
 
@@ -182,88 +190,83 @@ def _start_jobs(
182
190
 
183
191
  ### No action or --name was provided. Ask to start all stopped jobs.
184
192
  else:
185
- _running_daemons = get_running_daemons()
186
- _paused_daemons = get_paused_daemons()
187
- _stopped_daemons = get_stopped_daemons()
188
- if not _stopped_daemons and not _paused_daemons:
189
- if not _running_daemons:
190
- return False, "No jobs to start."
193
+ running_jobs = get_running_jobs(executor_keys, jobs, debug=debug)
194
+ paused_jobs = get_paused_jobs(executor_keys, jobs, debug=debug)
195
+ stopped_jobs = get_stopped_jobs(executor_keys, jobs, debug=debug)
196
+
197
+ if not stopped_jobs and not paused_jobs:
198
+ if not running_jobs:
199
+ return False, "No jobs to start"
191
200
  return True, "All jobs are running."
192
201
 
193
- names = [d.daemon_id for d in _stopped_daemons + _paused_daemons]
202
+ names = [
203
+ name
204
+ for name in list(stopped_jobs) + list(paused_jobs)
205
+ ]
194
206
 
195
207
  def _run_new_job(name: Optional[str] = None):
196
- kw['action'] = action
197
208
  name = name or get_new_daemon_name()
198
- kw['name'] = name
199
- _action_success_tuple = daemon_action(daemon_id=name, **kw)
200
- return _action_success_tuple, name
201
-
202
- def _run_existing_job(name: Optional[str] = None):
203
- daemon = Daemon(daemon_id=name)
204
- if daemon.process is not None:
205
- if daemon.status == 'paused':
206
- return daemon.resume(), daemon.daemon_id
207
- return (True, f"Job '{name}' is already running."), daemon.daemon_id
208
-
209
- if not daemon.path.exists():
210
- if not kw.get('nopretty', False):
211
- warn(f"There isn't a job with the name '{name}'.", stack=False)
212
- print(
213
- f"You can start a new job named '{name}' with `start job "
214
- + "{options}" + f" --name {name}`"
215
- )
216
- return (False, f"Job '{name}' does not exist."), daemon.daemon_id
209
+ job = Job(name, sysargs, executor_keys=executor_keys)
210
+ return job.start(debug=debug), name
217
211
 
218
- return daemon.run(allow_dirty_run=True), daemon.daemon_id
212
+ def _run_existing_job(name: str):
213
+ job = Job(name, executor_keys=executor_keys)
214
+ return job.start(debug=debug), name
219
215
 
220
216
  if not names:
221
217
  return False, "No jobs to start."
222
218
 
223
219
  ### Get user permission to clear logs.
224
- _filtered_daemons = get_filtered_daemons(names)
225
- if not kw.get('force', False) and _filtered_daemons:
226
- _filtered_running_daemons = get_running_daemons(_filtered_daemons)
227
- _skipped_daemons = []
228
- if _filtered_running_daemons:
229
- pprint_jobs(_filtered_running_daemons)
220
+ _filtered_jobs = get_filtered_jobs(executor_keys, names, debug=debug)
221
+ if not kw.get('force', False) and _filtered_jobs:
222
+ _filtered_running_jobs = get_running_jobs(executor_keys, _filtered_jobs, debug=debug)
223
+ _skipped_jobs = []
224
+ if _filtered_running_jobs:
225
+ pprint_jobs(_filtered_running_jobs)
230
226
  if yes_no(
231
227
  "Do you want to first stop these jobs?",
232
- default = 'n',
233
- yes = kw.get('yes', False),
234
- noask = kw.get('noask', False)
228
+ default='n',
229
+ yes=kw.get('yes', False),
230
+ noask=kw.get('noask', False)
235
231
  ):
236
232
  stop_success_tuple = actions['stop'](
237
- action = ['jobs'] + [d.daemon_id for d in _filtered_running_daemons],
238
- force = True,
233
+ action=['jobs'] + [_name for _name in _filtered_running_jobs],
234
+ force=True,
235
+ mrsm_instance=mrsm_instance,
236
+ debug=debug,
239
237
  )
240
238
  if not stop_success_tuple[0]:
241
239
  warn(
242
- "Failed to stop job" + ("s" if len(_filtered_running_daemons) != 1 else '')
243
- + items_str([d.daemon_id for d in _filtered_running_daemons])
244
- + ".",
245
- stack = False
240
+ (
241
+ "Failed to stop job"
242
+ + ("s" if len(_filtered_running_jobs) != 1 else '')
243
+ + items_str([_name for _name in _filtered_running_jobs])
244
+ + "."
245
+ ),
246
+ stack=False
246
247
  )
247
- for d in _filtered_running_daemons:
248
- names.remove(d.daemon_id)
249
- _filtered_daemons.remove(d)
248
+ for _name in _filtered_running_jobs:
249
+ names.remove(_name)
250
+ _filtered_jobs.pop(_name)
250
251
  else:
251
252
  info(
252
253
  "Skipping already running job"
253
- + ("s" if len(_filtered_running_daemons) != 1 else '') + ' '
254
- + items_str([d.daemon_id for d in _filtered_running_daemons]) + '.'
254
+ + ("s" if len(_filtered_running_jobs) != 1 else '')
255
+ + ' '
256
+ + items_str([_name for _name in _filtered_running_jobs])
257
+ + '.'
255
258
  )
256
- for d in _filtered_running_daemons:
257
- names.remove(d.daemon_id)
258
- _filtered_daemons.remove(d)
259
- _skipped_daemons.append(d)
259
+ for _name in _filtered_running_jobs:
260
+ names.remove(_name)
261
+ _filtered_jobs.pop(_name)
262
+ _skipped_jobs.append(_name)
260
263
 
261
- if not _filtered_daemons:
262
- return len(_skipped_daemons) > 0, "No jobs to start."
264
+ if not _filtered_jobs:
265
+ return len(_skipped_jobs) > 0, "No jobs to start."
263
266
 
264
- pprint_jobs(_filtered_daemons, nopretty=kw.get('nopretty', False))
267
+ pprint_jobs(_filtered_jobs, nopretty=kw.get('nopretty', False))
265
268
  info(
266
- f"Starting the job"
269
+ "Starting the job"
267
270
  + ("s" if len(names) != 1 else "")
268
271
  + " " + items_str(names)
269
272
  + "..."
@@ -278,7 +281,11 @@ def _start_jobs(
278
281
  )
279
282
  if not kw.get('nopretty', False):
280
283
  print_tuple(success_tuple)
281
- _successes.append(_name) if success_tuple[0] else _failures.append(_name)
284
+
285
+ if success_tuple[0]:
286
+ _successes.append(_name)
287
+ else:
288
+ _failures.append(_name)
282
289
 
283
290
  msg = (
284
291
  (("Successfully started job" + ("s" if len(_successes) != 1 else '')
@@ -289,25 +296,28 @@ def _start_jobs(
289
296
  )
290
297
  return len(_failures) == 0, msg
291
298
 
299
+
292
300
  def _complete_start_jobs(
293
- action: Optional[List[str]] = None,
294
- line: str = '',
295
- **kw
296
- ) -> List[str]:
297
- from meerschaum.utils.daemon import get_daemon_ids
298
- daemon_ids = get_daemon_ids()
301
+ action: Optional[List[str]] = None,
302
+ executor_keys: Optional[str] = None,
303
+ line: str = '',
304
+ **kw
305
+ ) -> List[str]:
306
+ from meerschaum.utils.jobs import get_filtered_jobs
307
+ jobs = get_filtered_jobs(executor_keys, action)
299
308
  if not action:
300
- return daemon_ids
309
+ return list(jobs)
310
+
301
311
  possibilities = []
302
312
  _line_end = line.split(' ')[-1]
303
- for daemon_id in daemon_ids:
304
- if daemon_id in action:
313
+ for name in jobs:
314
+ if name in action:
305
315
  continue
306
316
  if _line_end == '':
307
- possibilities.append(daemon_id)
317
+ possibilities.append(name)
308
318
  continue
309
- if daemon_id.startswith(action[-1]):
310
- possibilities.append(daemon_id)
319
+ if name.startswith(action[-1]):
320
+ possibilities.append(name)
311
321
  return possibilities
312
322
 
313
323
 
@@ -50,11 +50,13 @@ def _complete_stop(
50
50
 
51
51
  def _stop_jobs(
52
52
  action: Optional[List[str]] = None,
53
+ executor_keys: Optional[str] = None,
53
54
  timeout_seconds: Optional[int] = None,
54
55
  noask: bool = False,
55
56
  force: bool = False,
56
57
  yes: bool = False,
57
58
  nopretty: bool = False,
59
+ debug: bool = False,
58
60
  **kw
59
61
  ) -> SuccessTuple:
60
62
  """
@@ -62,66 +64,93 @@ def _stop_jobs(
62
64
 
63
65
  To see running processes, run `show jobs`.
64
66
  """
67
+ from meerschaum.utils.jobs import (
68
+ get_filtered_jobs,
69
+ get_running_jobs,
70
+ get_paused_jobs,
71
+ get_stopped_jobs,
72
+ get_restart_jobs,
73
+ )
65
74
  from meerschaum.utils.formatting._jobs import pprint_jobs
66
75
  from meerschaum.utils.daemon import (
67
76
  get_filtered_daemons, get_running_daemons, get_stopped_daemons, get_paused_daemons,
68
77
  )
69
78
  from meerschaum.utils.warnings import warn
70
79
  from meerschaum.utils.prompt import yes_no
71
- daemons = get_filtered_daemons(action, warn=(not nopretty))
72
- _running_daemons = get_running_daemons(daemons)
73
- _paused_daemons = get_paused_daemons(daemons)
74
- _stopped_daemons = get_stopped_daemons(daemons)
75
- if action and _stopped_daemons and not nopretty:
80
+
81
+ jobs = get_filtered_jobs(executor_keys, action, warn=(not nopretty))
82
+ running_jobs = get_running_jobs(executor_keys, jobs)
83
+ paused_jobs = get_paused_jobs(executor_keys, jobs)
84
+ restart_jobs = get_restart_jobs(executor_keys, jobs)
85
+ stopped_jobs = {
86
+ name: job
87
+ for name, job in get_stopped_jobs(executor_keys, jobs).items()
88
+ if name not in restart_jobs
89
+ }
90
+
91
+ jobs_to_stop = {
92
+ **running_jobs,
93
+ **paused_jobs,
94
+ **restart_jobs,
95
+ }
96
+
97
+ if action and stopped_jobs and not nopretty:
76
98
  warn(
77
- f"Skipping stopped job" + ("s" if len(_stopped_daemons) > 1 else '') + " '" +
78
- ("', '".join(d.daemon_id for d in _stopped_daemons)) + "'.",
79
- stack = False
99
+ "Skipping stopped job"
100
+ + ("s" if len(stopped_jobs) != 1 else '')
101
+ + " '"
102
+ + ("', '".join(name for name in stopped_jobs)) + "'.",
103
+ stack=False,
80
104
  )
81
105
 
82
- daemons_to_stop = _running_daemons + _paused_daemons
83
- if not daemons_to_stop:
84
- return False, "No running or paused jobs to stop."
106
+ if not jobs_to_stop:
107
+ return False, "No running, paused or restarting jobs to stop."
85
108
 
86
109
  if not action:
87
110
  if not force:
88
- pprint_jobs(daemons_to_stop)
111
+ pprint_jobs(jobs_to_stop)
89
112
  if not yes_no(
90
113
  "Stop the above jobs?",
91
114
  noask=noask, yes=yes, default='n'
92
115
  ):
93
116
  return False, "No jobs were stopped."
94
117
 
95
- _quit_daemons, _kill_daemons = [], []
96
- for d in daemons_to_stop:
97
- quit_success, quit_msg = d.quit(timeout=timeout_seconds)
98
- if quit_success:
99
- _quit_daemons.append(d)
100
- continue
101
- else:
102
- warn(
103
- f"Failed to gracefully quit job '{d.daemon_id}', attempting to terminate:\n "
104
- + f"{quit_msg}",
105
- stack = False,
106
- )
107
-
108
- kill_success, kill_msg = d.kill(timeout=timeout_seconds)
109
- if kill_success:
110
- _kill_daemons.append(d)
111
- continue
112
- if not nopretty:
113
- warn(f"Failed to kill job '{d.daemon_id}' (PID {d.pid}):\n{kill_msg}", stack=False)
118
+ job_success_tuples = {}
119
+ for name, job in jobs_to_stop.items():
120
+ stop_success, stop_msg = job.stop(
121
+ timeout_seconds=timeout_seconds,
122
+ debug=debug,
123
+ )
124
+ job_success_tuples[name] = (stop_success, stop_msg)
114
125
 
126
+ num_success = sum(
127
+ (
128
+ 1
129
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
130
+ if stop_success
131
+ )
132
+ )
133
+ num_fail = sum(
134
+ (
135
+ 1
136
+ for name, (stop_success, stop_msg) in job_success_tuples.items()
137
+ if not stop_success
138
+ )
139
+ )
140
+ success = num_success > 0
115
141
  msg = (
116
- (("Stopped job" + ("s" if len(_quit_daemons) != 1 else '') +
117
- " '" + "', '".join([d.daemon_id for d in _quit_daemons]) + "'.")
118
- if _quit_daemons else '')
119
- + (("\n" if _quit_daemons else "")
120
- + ("Killed job" + ("s" if len(_kill_daemons) != 1 else '') +
121
- " '" + "', '".join([d.daemon_id for d in _kill_daemons]) + "'.")
122
- if _kill_daemons else '')
142
+ f"Stopped {num_success} job"
143
+ + ('s' if num_success != 1 else '')
144
+ + '.'
123
145
  )
124
- return (len(_quit_daemons + _kill_daemons) > 0), msg
146
+ if num_fail > 0:
147
+ msg += (
148
+ f"\nFailed to stop {num_fail} job"
149
+ + ('s' if num_fail != 1 else '')
150
+ + '.'
151
+ )
152
+
153
+ return success, msg
125
154
 
126
155
 
127
156
  ### NOTE: This must be the final statement of the module.
meerschaum/api/_events.py CHANGED
@@ -19,14 +19,20 @@ from meerschaum.utils.debug import dprint
19
19
  from meerschaum.connectors.poll import retry_connect
20
20
  from meerschaum.utils.warnings import warn
21
21
  from meerschaum._internal.term.tools import is_webterm_running
22
+ from meerschaum.utils.jobs import start_check_jobs_thread, stop_check_jobs_thread
23
+
24
+ _check_jobs_thread = None
22
25
 
23
26
  @app.on_event("startup")
24
27
  async def startup():
25
- conn = get_api_connector()
28
+ """
29
+ Connect to the instance database and begin monitoring jobs.
30
+ """
26
31
  try:
27
32
  if not no_dash:
28
33
  from meerschaum.api.dash.webterm import start_webterm
29
34
  start_webterm()
35
+
30
36
  connected = retry_connect(
31
37
  get_api_connector(),
32
38
  workers = get_uvicorn_config().get('workers', None),
@@ -36,18 +42,29 @@ async def startup():
36
42
  import traceback
37
43
  traceback.print_exc()
38
44
  connected = False
45
+
39
46
  if not connected:
40
47
  await shutdown()
41
48
  os._exit(1)
42
49
 
50
+ start_check_jobs_thread()
51
+
43
52
 
44
53
  @app.on_event("shutdown")
45
54
  async def shutdown():
55
+ """
56
+ Close the database connection and stop monitoring jobs.
57
+ """
46
58
  if debug:
47
59
  dprint("Closing connection...")
48
60
  if get_api_connector().type == 'sql':
49
61
  get_api_connector().engine.dispose()
50
62
 
63
+ stop_check_jobs_thread()
64
+ from meerschaum.api.routes._actions import _temp_jobs
65
+ for name, job in _temp_jobs.items():
66
+ job.delete()
67
+
51
68
  ### Terminate any running jobs left over.
52
69
  if 'meerschaum.api.dash' in sys.modules:
53
70
  from meerschaum.api.dash.actions import running_jobs, stop_action
meerschaum/api/_oauth2.py CHANGED
@@ -30,6 +30,7 @@ class CustomOAuth2PasswordRequestForm:
30
30
  self.client_id = client_id
31
31
  self.client_secret = client_secret
32
32
 
33
+
33
34
  LoginManager = fastapi_login.LoginManager
34
35
  def generate_secret_key() -> str:
35
36
  """
@@ -46,5 +47,6 @@ def generate_secret_key() -> str:
46
47
 
47
48
  return secret_key.encode('utf-8')
48
49
 
50
+
49
51
  SECRET = generate_secret_key()
50
52
  manager = LoginManager(SECRET, token_url=endpoints['login'])
@@ -23,8 +23,8 @@ sessions = {}
23
23
  @app.websocket('/dash/ws')
24
24
  @app.websocket(_websocket_endpoint)
25
25
  async def websocket_endpoint(
26
- websocket: fastapi.WebSocket,
27
- ):
26
+ websocket: fastapi.WebSocket,
27
+ ):
28
28
  """
29
29
  Communicate with the Web Interface over a websocket.
30
30
  """
@@ -16,6 +16,7 @@ from meerschaum.api import CHECK_UPDATE
16
16
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
17
17
  html, dcc = import_html(), import_dcc()
18
18
  dateutil_parser = attempt_import('dateutil.parser', check_update=CHECK_UPDATE)
19
+ from meerschaum.utils.jobs import get_jobs, Job
19
20
  from meerschaum.utils.daemon import (
20
21
  get_daemons,
21
22
  get_running_daemons,
@@ -38,13 +39,15 @@ def get_jobs_cards(state: WebState):
38
39
  Build cards and alerts lists for jobs.
39
40
  """
40
41
  daemons = get_daemons()
42
+ jobs = get_jobs()
41
43
  session_id = state['session-store.data'].get('session-id', None)
42
44
  is_authenticated = is_session_authenticated(session_id)
43
45
 
44
46
  alert = alert_from_success_tuple(daemons)
45
47
  cards = []
46
48
 
47
- for d in daemons:
49
+ for name, job in jobs.items():
50
+ d = job.daemon
48
51
  footer_children = html.Div(
49
52
  build_process_timestamps_children(d),
50
53
  id = {'type': 'process-timestamps-div', 'index': d.daemon_id},
@@ -80,7 +83,7 @@ def get_jobs_cards(state: WebState):
80
83
  ]
81
84
 
82
85
  body_children = [
83
- html.H4(html.B(d.daemon_id), className="card-title"),
86
+ html.H4(html.B(name), className="card-title"),
84
87
  html.Div(
85
88
  html.P(
86
89
  d.label,
@@ -8,6 +8,7 @@ Import all routes from other modules in package
8
8
 
9
9
  from meerschaum.api.routes._login import *
10
10
  from meerschaum.api.routes._actions import *
11
+ from meerschaum.api.routes._jobs import *
11
12
  from meerschaum.api.routes._connectors import *
12
13
  from meerschaum.api.routes._index import *
13
14
  from meerschaum.api.routes._misc import *