meerschaum 2.4.11__py3-none-any.whl → 2.4.13__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 (41) hide show
  1. meerschaum/_internal/arguments/_parse_arguments.py +15 -1
  2. meerschaum/_internal/docs/index.py +1 -0
  3. meerschaum/_internal/entry.py +1 -0
  4. meerschaum/_internal/shell/Shell.py +19 -9
  5. meerschaum/_internal/shell/ShellCompleter.py +11 -6
  6. meerschaum/actions/bootstrap.py +119 -16
  7. meerschaum/actions/clear.py +41 -30
  8. meerschaum/actions/delete.py +1 -1
  9. meerschaum/actions/edit.py +118 -3
  10. meerschaum/actions/sh.py +11 -10
  11. meerschaum/actions/start.py +61 -4
  12. meerschaum/actions/sync.py +14 -16
  13. meerschaum/actions/upgrade.py +5 -4
  14. meerschaum/api/dash/callbacks/dashboard.py +2 -1
  15. meerschaum/api/dash/callbacks/jobs.py +53 -7
  16. meerschaum/api/dash/callbacks/pipes.py +1 -1
  17. meerschaum/api/dash/jobs.py +86 -60
  18. meerschaum/api/dash/pages/__init__.py +1 -0
  19. meerschaum/api/dash/pages/job.py +21 -0
  20. meerschaum/api/routes/_jobs.py +3 -3
  21. meerschaum/config/_version.py +1 -1
  22. meerschaum/connectors/sql/_fetch.py +67 -61
  23. meerschaum/connectors/sql/_pipes.py +36 -29
  24. meerschaum/plugins/__init__.py +6 -2
  25. meerschaum/plugins/bootstrap.py +15 -15
  26. meerschaum/utils/formatting/__init__.py +32 -16
  27. meerschaum/utils/formatting/_pipes.py +1 -1
  28. meerschaum/utils/formatting/_shell.py +4 -3
  29. meerschaum/utils/process.py +18 -8
  30. meerschaum/utils/prompt.py +16 -15
  31. meerschaum/utils/sql.py +107 -35
  32. meerschaum/utils/venv/__init__.py +35 -24
  33. meerschaum/utils/warnings.py +7 -7
  34. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/METADATA +1 -1
  35. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/RECORD +41 -40
  36. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/WHEEL +1 -1
  37. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/LICENSE +0 -0
  38. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/NOTICE +0 -0
  39. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/entry_points.txt +0 -0
  40. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/top_level.txt +0 -0
  41. {meerschaum-2.4.11.dist-info → meerschaum-2.4.13.dist-info}/zip-safe +0 -0
@@ -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,30 @@ 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
+ if definition is None:
313
+ raise ValueError(f"No SQL definition could be found for {pipe}.")
314
+ return format_cte_subquery(definition, flavor, 'definition')
315
+
312
316
 
313
317
  def _join_fetch_query(
314
- pipe,
315
- debug: bool = False,
316
- new_ids: bool = True,
317
- **kw
318
- ) -> str:
318
+ pipe: mrsm.Pipe,
319
+ flavor: str,
320
+ debug: bool = False,
321
+ new_ids: bool = True,
322
+ **kw
323
+ ) -> str:
319
324
  """Build a fetch query based on the datetime and ID indices."""
320
325
  if not pipe.exists(debug=debug):
321
- return _simple_fetch_query(pipe, debug=debug, **kw)
326
+ return _simple_fetch_query(pipe, flavor, debug=debug, **kw)
322
327
 
323
328
  from meerschaum.utils.sql import sql_item_name, dateadd_str
324
329
  pipe_instance_name = sql_item_name(
@@ -350,7 +355,8 @@ def _join_fetch_query(
350
355
  """
351
356
  sync_times = pipe.instance_connector.read(sync_times_query, debug=debug, silent=False)
352
357
  if sync_times is None:
353
- return _simple_fetch_query(pipe, debug=debug, **kw)
358
+ return _simple_fetch_query(pipe, flavor, debug=debug, **kw)
359
+
354
360
  _sync_times_q = f",\n{sync_times_remote_name} AS ("
355
361
  for _id, _st in sync_times.itertuples(index=False):
356
362
  _sync_times_q += (
@@ -2096,7 +2096,7 @@ def get_pipe_rowcount(
2096
2096
  An `int` for the number of rows if the `pipe` exists, otherwise `None`.
2097
2097
 
2098
2098
  """
2099
- from meerschaum.utils.sql import dateadd_str, sql_item_name, NO_CTE_FLAVORS
2099
+ from meerschaum.utils.sql import dateadd_str, sql_item_name, wrap_query_with_cte
2100
2100
  from meerschaum.connectors.sql._fetch import get_pipe_query
2101
2101
  if remote:
2102
2102
  msg = f"'fetch:definition' must be an attribute of {pipe} to get a remote rowcount."
@@ -2175,20 +2175,10 @@ def get_pipe_rowcount(
2175
2175
  if not remote
2176
2176
  else get_pipe_query(pipe)
2177
2177
  )
2178
- query = (
2179
- f"""
2180
- WITH src AS ({src})
2181
- SELECT COUNT(*)
2182
- FROM src
2183
- """
2184
- ) if self.flavor not in ('mysql', 'mariadb') else (
2185
- f"""
2186
- SELECT COUNT(*)
2187
- FROM ({src}) AS src
2188
- """
2189
- )
2178
+ parent_query = f"SELECT COUNT(*)\nFROM {sql_item_name('src', self.flavor)}"
2179
+ query = wrap_query_with_cte(src, parent_query, self.flavor)
2190
2180
  if begin is not None or end is not None:
2191
- query += "WHERE"
2181
+ query += "\nWHERE"
2192
2182
  if begin is not None:
2193
2183
  query += f"""
2194
2184
  {dt} >= {dateadd_str(self.flavor, datepart='minute', number=0, begin=begin)}
@@ -2330,10 +2320,10 @@ def clear_pipe(
2330
2320
 
2331
2321
 
2332
2322
  def get_pipe_table(
2333
- self,
2334
- pipe: mrsm.Pipe,
2335
- debug: bool = False,
2336
- ) -> sqlalchemy.Table:
2323
+ self,
2324
+ pipe: mrsm.Pipe,
2325
+ debug: bool = False,
2326
+ ) -> Union['sqlalchemy.Table', None]:
2337
2327
  """
2338
2328
  Return the `sqlalchemy.Table` object for a `mrsm.Pipe`.
2339
2329
 
@@ -2352,18 +2342,18 @@ def get_pipe_table(
2352
2342
  return None
2353
2343
  return get_sqlalchemy_table(
2354
2344
  pipe.target,
2355
- connector = self,
2356
- schema = self.get_pipe_schema(pipe),
2357
- debug = debug,
2358
- refresh = True,
2345
+ connector=self,
2346
+ schema=self.get_pipe_schema(pipe),
2347
+ debug=debug,
2348
+ refresh=True,
2359
2349
  )
2360
2350
 
2361
2351
 
2362
2352
  def get_pipe_columns_types(
2363
- self,
2364
- pipe: mrsm.Pipe,
2365
- debug: bool = False,
2366
- ) -> Dict[str, str]:
2353
+ self,
2354
+ pipe: mrsm.Pipe,
2355
+ debug: bool = False,
2356
+ ) -> Dict[str, str]:
2367
2357
  """
2368
2358
  Get the pipe's columns and types.
2369
2359
 
@@ -2394,8 +2384,8 @@ def get_pipe_columns_types(
2394
2384
  return get_table_cols_types(
2395
2385
  pipe.target,
2396
2386
  self,
2397
- flavor = self.flavor,
2398
- schema = self.schema,
2387
+ flavor=self.flavor,
2388
+ schema=self.get_pipe_schema(pipe),
2399
2389
  )
2400
2390
 
2401
2391
  table_columns = {}
@@ -2448,6 +2438,7 @@ def get_add_columns_queries(
2448
2438
  from meerschaum.utils.sql import (
2449
2439
  sql_item_name,
2450
2440
  SINGLE_ALTER_TABLE_FLAVORS,
2441
+ get_table_cols_types,
2451
2442
  )
2452
2443
  from meerschaum.utils.dtypes.sql import (
2453
2444
  get_pd_type_from_db_type,
@@ -2480,6 +2471,14 @@ def get_add_columns_queries(
2480
2471
  db_cols_types = {
2481
2472
  col: get_pd_type_from_db_type(str(typ.type))
2482
2473
  for col, typ in table_obj.columns.items()
2474
+ } if table_obj is not None else {
2475
+ col: get_pd_type_from_db_type(typ)
2476
+ for col, typ in get_table_cols_types(
2477
+ pipe.target,
2478
+ self,
2479
+ schema=self.get_pipe_schema(pipe),
2480
+ debug=debug,
2481
+ ).items()
2483
2482
  }
2484
2483
  new_cols = set(df_cols_types) - set(db_cols_types)
2485
2484
  if not new_cols:
@@ -2552,7 +2551,7 @@ def get_alter_columns_queries(
2552
2551
  """
2553
2552
  if not pipe.exists(debug=debug):
2554
2553
  return []
2555
- from meerschaum.utils.sql import sql_item_name, DROP_IF_EXISTS_FLAVORS
2554
+ from meerschaum.utils.sql import sql_item_name, DROP_IF_EXISTS_FLAVORS, get_table_cols_types
2556
2555
  from meerschaum.utils.dataframe import get_numeric_cols
2557
2556
  from meerschaum.utils.dtypes import are_dtypes_equal
2558
2557
  from meerschaum.utils.dtypes.sql import (
@@ -2583,6 +2582,14 @@ def get_alter_columns_queries(
2583
2582
  db_cols_types = {
2584
2583
  col: get_pd_type_from_db_type(str(typ.type))
2585
2584
  for col, typ in table_obj.columns.items()
2585
+ } if table_obj is not None else {
2586
+ col: get_pd_type_from_db_type(typ)
2587
+ for col, typ in get_table_cols_types(
2588
+ pipe.target,
2589
+ self,
2590
+ schema=self.get_pipe_schema(pipe),
2591
+ debug=debug,
2592
+ ).items()
2586
2593
  }
2587
2594
  pipe_bool_cols = [col for col, typ in pipe.dtypes.items() if are_dtypes_equal(str(typ), 'bool')]
2588
2595
  pd_db_df_aliases = {
@@ -316,7 +316,11 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
316
316
 
317
317
  ### NOTE: Allow plugins to be installed via `pip`.
318
318
  packaged_plugin_paths = []
319
- discovered_packaged_plugins_eps = entry_points(group='meerschaum.plugins')
319
+ try:
320
+ discovered_packaged_plugins_eps = entry_points(group='meerschaum.plugins')
321
+ except TypeError:
322
+ discovered_packaged_plugins_eps = []
323
+
320
324
  for ep in discovered_packaged_plugins_eps:
321
325
  module_name = ep.name
322
326
  for package_file_path in ep.dist.files:
@@ -330,7 +334,7 @@ def sync_plugins_symlinks(debug: bool = False, warn: bool = True) -> None:
330
334
  if is_symlink(PLUGINS_RESOURCES_PATH) or not PLUGINS_RESOURCES_PATH.exists():
331
335
  try:
332
336
  PLUGINS_RESOURCES_PATH.unlink()
333
- except Exception as e:
337
+ except Exception:
334
338
  pass
335
339
 
336
340
  PLUGINS_RESOURCES_PATH.mkdir(exist_ok=True)
@@ -47,7 +47,6 @@ IMPORTS_LINES: Dict[str, str] = {
47
47
  ),
48
48
  }
49
49
 
50
- ### TODO: Add feature for custom connectors.
51
50
  FEATURE_LINES: Dict[str, str] = {
52
51
  'header': (
53
52
  "#! /usr/bin/env python3\n"
@@ -94,7 +93,7 @@ FEATURE_LINES: Dict[str, str] = {
94
93
  "class {plugin_name_capitalized}Connector(Connector):\n"
95
94
  " \"\"\"Implement '{plugin_name_lower}' connectors.\"\"\"\n\n"
96
95
  " REQUIRED_ATTRIBUTES: list[str] = []\n"
97
- " \n"
96
+ "\n"
98
97
  " def fetch(\n"
99
98
  " self,\n"
100
99
  " pipe: mrsm.Pipe,\n"
@@ -149,11 +148,12 @@ FEATURE_LINES: Dict[str, str] = {
149
148
  ),
150
149
  }
151
150
 
151
+
152
152
  def bootstrap_plugin(
153
- plugin_name: str,
154
- debug: bool = False,
155
- **kwargs: Any
156
- ) -> SuccessTuple:
153
+ plugin_name: str,
154
+ debug: bool = False,
155
+ **kwargs: Any
156
+ ) -> SuccessTuple:
157
157
  """
158
158
  Prompt the user for features and create a plugin file.
159
159
  """
@@ -177,9 +177,9 @@ def bootstrap_plugin(
177
177
  features: List[str] = choose(
178
178
  "Which of the following features would you like to add to your plugin?",
179
179
  list(FEATURE_CHOICES.items()),
180
- default = 'fetch',
181
- multiple = True,
182
- as_indices = True,
180
+ default='fetch',
181
+ multiple=True,
182
+ as_indices=True,
183
183
  **kwargs
184
184
  )
185
185
 
@@ -256,7 +256,7 @@ def bootstrap_plugin(
256
256
  _ = prompt(
257
257
  f"Press [Enter] to edit plugin '{plugin_name}',"
258
258
  + " [CTRL+C] to skip.",
259
- icon = False,
259
+ icon=False,
260
260
  )
261
261
  except (KeyboardInterrupt, Exception):
262
262
  return True, "Success"
@@ -267,7 +267,7 @@ def bootstrap_plugin(
267
267
 
268
268
  def _get_plugins_dir_path() -> pathlib.Path:
269
269
  from meerschaum.config.paths import PLUGINS_DIR_PATHS
270
-
270
+
271
271
  if not PLUGINS_DIR_PATHS:
272
272
  raise EnvironmentError("No plugin dir path could be found.")
273
273
 
@@ -278,9 +278,9 @@ def _get_plugins_dir_path() -> pathlib.Path:
278
278
  choose(
279
279
  "In which directory do you want to write your plugin?",
280
280
  [path.as_posix() for path in PLUGINS_DIR_PATHS],
281
- numeric = True,
282
- multiple = False,
283
- default = PLUGINS_DIR_PATHS[0].as_posix(),
281
+ numeric=True,
282
+ multiple=False,
283
+ default=PLUGINS_DIR_PATHS[0].as_posix(),
284
284
  )
285
285
  )
286
286
 
@@ -290,7 +290,7 @@ def _ask_to_uninstall(plugin: mrsm.Plugin, **kwargs: Any) -> SuccessTuple:
290
290
  warn(f"Plugin '{plugin}' is already installed!", stack=False)
291
291
  uninstall_plugin = yes_no(
292
292
  f"Do you want to first uninstall '{plugin}'?",
293
- default = 'n',
293
+ default='n',
294
294
  **kwargs
295
295
  )
296
296
  if not uninstall_plugin:
@@ -11,7 +11,7 @@ import platform
11
11
  import os
12
12
  import sys
13
13
  import meerschaum as mrsm
14
- from meerschaum.utils.typing import Optional, Union, Any, Dict
14
+ from meerschaum.utils.typing import Optional, Union, Any, Dict, Iterable
15
15
  from meerschaum.utils.formatting._shell import make_header
16
16
  from meerschaum.utils.formatting._pprint import pprint
17
17
  from meerschaum.utils.formatting._pipes import (
@@ -322,14 +322,14 @@ def format_success_tuple(
322
322
  from meerschaum.config import get_config
323
323
  status_config = get_config('formatting', status, patch=True)
324
324
 
325
- msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(tup[1])
325
+ msg = (' ' * left_padding) + status_config[CHARSET]['icon'] + ' ' + str(highlight_pipes(tup[1]))
326
326
  lines = msg.split('\n')
327
327
  lines = [lines[0]] + [
328
328
  ((' ' + line if not line.startswith(' ') else line))
329
329
  for line in lines[1:]
330
330
  ]
331
331
  if ANSI:
332
- lines[0] = fill_ansi(highlight_pipes(lines[0]), **status_config['ansi']['rich'])
332
+ lines[0] = fill_ansi(lines[0], **status_config['ansi']['rich'])
333
333
 
334
334
  msg = '\n'.join(lines)
335
335
  msg = ('\n' * upper_padding) + msg + ('\n' * lower_padding)
@@ -337,7 +337,7 @@ def format_success_tuple(
337
337
 
338
338
 
339
339
  def print_options(
340
- options: Optional[Dict[str, Any]] = None,
340
+ options: Optional[Iterable[Any]] = None,
341
341
  nopretty: bool = False,
342
342
  no_rich: bool = False,
343
343
  name: str = 'options',
@@ -345,6 +345,7 @@ def print_options(
345
345
  num_cols: Optional[int] = None,
346
346
  adjust_cols: bool = True,
347
347
  sort_options: bool = False,
348
+ number_options: bool = False,
348
349
  **kw
349
350
  ) -> None:
350
351
  """
@@ -373,6 +374,12 @@ def print_options(
373
374
  adjust_cols: bool, default True
374
375
  If `True`, adjust the number of columns depending on the terminal size.
375
376
 
377
+ sort_options: bool, default False
378
+ If `True`, print the options in sorted order.
379
+
380
+ number_options: bool, default False
381
+ If `True`, print the option's number in the list (1 index).
382
+
376
383
  """
377
384
  import os
378
385
  from meerschaum.utils.packages import import_rich
@@ -398,9 +405,10 @@ def print_options(
398
405
  print()
399
406
  print(make_header(_header))
400
407
  ### print actions
401
- for option in _options:
408
+ for i, option in enumerate(_options):
409
+ marker = '-' if not number_options else (str(i + 1) + '.')
402
410
  if not nopretty:
403
- print(" - ", end="")
411
+ print(f" {marker} ", end="")
404
412
  print(option)
405
413
  if not nopretty:
406
414
  print()
@@ -434,11 +442,11 @@ def print_options(
434
442
 
435
443
  if _header is not None:
436
444
  table = Table(
437
- title = _header,
438
- box = box.SIMPLE,
439
- show_header = False,
440
- show_footer = False,
441
- title_style = '',
445
+ title=_header,
446
+ box=box.SIMPLE,
447
+ show_header=False,
448
+ show_footer=False,
449
+ title_style='',
442
450
  expand = True,
443
451
  )
444
452
  else:
@@ -447,18 +455,26 @@ def print_options(
447
455
  table.add_column()
448
456
 
449
457
  if len(_options) < 12:
450
- # If fewer than 12 items, use a single column
451
- for option in _options:
452
- table.add_row(Text.from_ansi(highlight_pipes(option)))
458
+ ### If fewer than 12 items, use a single column
459
+ for i, option in enumerate(_options):
460
+ item = highlight_pipes(option)
461
+ if number_options:
462
+ item = str(i + 1) + '. ' + item
463
+ table.add_row(Text.from_ansi(item))
453
464
  else:
454
- # Otherwise, use multiple columns as before
465
+ ### Otherwise, use multiple columns as before
455
466
  num_rows = (len(_options) + num_cols - 1) // num_cols
467
+ item_ix = 0
456
468
  for i in range(num_rows):
457
469
  row = []
458
470
  for j in range(num_cols):
459
471
  index = i + j * num_rows
460
472
  if index < len(_options):
461
- row.append(Text.from_ansi(highlight_pipes(_options[index])))
473
+ item = highlight_pipes(_options[index])
474
+ if number_options:
475
+ item = str(i + 1) + '. ' + item
476
+ row.append(Text.from_ansi(item))
477
+ item_ix += 1
462
478
  else:
463
479
  row.append('')
464
480
  table.add_row(*row)
@@ -323,7 +323,7 @@ def pipe_repr(
323
323
  )
324
324
  if as_rich_text:
325
325
  return text_obj
326
- return rich_text_to_str(text_obj)
326
+ return rich_text_to_str(text_obj).replace('\n', '')
327
327
 
328
328
 
329
329
  def highlight_pipes(message: str) -> str:
@@ -10,10 +10,11 @@ from re import sub
10
10
  from meerschaum.utils.threading import Lock
11
11
  _locks = {'_tried_clear_command': Lock()}
12
12
 
13
+
13
14
  def make_header(
14
- message : str,
15
- ruler : str = '─',
16
- ) -> str:
15
+ message: str,
16
+ ruler: str = '─',
17
+ ) -> str:
17
18
  """Format a message string with a ruler.
18
19
  Length of the ruler is the length of the longest word.
19
20
 
@@ -178,22 +178,32 @@ def run_process(
178
178
 
179
179
  return ret
180
180
 
181
+
181
182
  def poll_process(
182
- proc: subprocess.Popen,
183
- line_callback: Callable[[bytes], Any],
184
- timeout_seconds: Union[int, float, None] = None,
185
- timeout_callback: Optional[Callable[[Any], Any]] = None,
186
- timeout_callback_args: Optional[Tuple[Any]] = None,
187
- timeout_callback_kwargs: Optional[Dict[str, Any]] = None,
188
- ) -> int:
183
+ proc: subprocess.Popen,
184
+ line_callback: Callable[[bytes], Any],
185
+ timeout_seconds: Union[int, float, None] = None,
186
+ timeout_callback: Optional[Callable[[Any], Any]] = None,
187
+ timeout_callback_args: Optional[Tuple[Any]] = None,
188
+ timeout_callback_kwargs: Optional[Dict[str, Any]] = None,
189
+ ) -> int:
189
190
  """
190
191
  Poll a process and execute a callback function for each line printed to the process's `stdout`.
191
192
  """
192
193
  from meerschaum.utils.threading import Timer
194
+ from meerschaum.utils.warnings import warn
193
195
 
194
196
  def timeout_handler():
195
197
  nonlocal timeout_callback_args, timeout_callback_kwargs
196
- proc.terminate()
198
+ try:
199
+ if platform.system() != 'Windows':
200
+ ### The process being killed may have children.
201
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
202
+ else:
203
+ proc.send_signal(signal.CTRL_BREAK_EVENT)
204
+ proc.terminate()
205
+ except Exception as e:
206
+ warn(f"Failed to kill process:\n{e}")
197
207
  if timeout_callback_args is None:
198
208
  timeout_callback_args = []
199
209
  if timeout_callback_kwargs is None: