meerschaum 2.9.4__py3-none-any.whl → 3.0.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 (201) hide show
  1. meerschaum/__init__.py +5 -2
  2. meerschaum/_internal/__init__.py +1 -0
  3. meerschaum/_internal/arguments/_parse_arguments.py +4 -4
  4. meerschaum/_internal/arguments/_parser.py +33 -4
  5. meerschaum/_internal/cli/__init__.py +6 -0
  6. meerschaum/_internal/cli/daemons.py +103 -0
  7. meerschaum/_internal/cli/entry.py +220 -0
  8. meerschaum/_internal/cli/workers.py +435 -0
  9. meerschaum/_internal/docs/index.py +48 -2
  10. meerschaum/_internal/entry.py +50 -14
  11. meerschaum/_internal/shell/Shell.py +121 -29
  12. meerschaum/_internal/shell/__init__.py +4 -1
  13. meerschaum/_internal/static.py +359 -0
  14. meerschaum/_internal/term/TermPageHandler.py +1 -2
  15. meerschaum/_internal/term/__init__.py +40 -6
  16. meerschaum/_internal/term/tools.py +33 -8
  17. meerschaum/actions/__init__.py +6 -4
  18. meerschaum/actions/api.py +53 -13
  19. meerschaum/actions/attach.py +1 -0
  20. meerschaum/actions/bootstrap.py +8 -8
  21. meerschaum/actions/delete.py +4 -2
  22. meerschaum/actions/edit.py +171 -25
  23. meerschaum/actions/login.py +8 -8
  24. meerschaum/actions/register.py +143 -6
  25. meerschaum/actions/reload.py +22 -5
  26. meerschaum/actions/restart.py +14 -0
  27. meerschaum/actions/show.py +184 -31
  28. meerschaum/actions/start.py +166 -17
  29. meerschaum/actions/stop.py +38 -2
  30. meerschaum/actions/sync.py +7 -2
  31. meerschaum/actions/tag.py +9 -8
  32. meerschaum/actions/verify.py +5 -8
  33. meerschaum/api/__init__.py +45 -15
  34. meerschaum/api/_events.py +46 -4
  35. meerschaum/api/_oauth2.py +162 -9
  36. meerschaum/api/_tokens.py +102 -0
  37. meerschaum/api/dash/__init__.py +0 -3
  38. meerschaum/api/dash/callbacks/__init__.py +1 -0
  39. meerschaum/api/dash/callbacks/custom.py +4 -3
  40. meerschaum/api/dash/callbacks/dashboard.py +228 -117
  41. meerschaum/api/dash/callbacks/jobs.py +14 -7
  42. meerschaum/api/dash/callbacks/login.py +10 -1
  43. meerschaum/api/dash/callbacks/pipes.py +194 -14
  44. meerschaum/api/dash/callbacks/plugins.py +0 -1
  45. meerschaum/api/dash/callbacks/register.py +10 -3
  46. meerschaum/api/dash/callbacks/settings/password_reset.py +2 -2
  47. meerschaum/api/dash/callbacks/tokens.py +389 -0
  48. meerschaum/api/dash/components.py +36 -15
  49. meerschaum/api/dash/jobs.py +1 -1
  50. meerschaum/api/dash/keys.py +35 -93
  51. meerschaum/api/dash/pages/__init__.py +2 -1
  52. meerschaum/api/dash/pages/dashboard.py +1 -20
  53. meerschaum/api/dash/pages/{job.py → jobs.py} +10 -7
  54. meerschaum/api/dash/pages/login.py +2 -2
  55. meerschaum/api/dash/pages/pipes.py +16 -5
  56. meerschaum/api/dash/pages/settings/password_reset.py +1 -1
  57. meerschaum/api/dash/pages/tokens.py +53 -0
  58. meerschaum/api/dash/pipes.py +438 -88
  59. meerschaum/api/dash/sessions.py +12 -0
  60. meerschaum/api/dash/tokens.py +603 -0
  61. meerschaum/api/dash/websockets.py +1 -1
  62. meerschaum/api/dash/webterm.py +18 -6
  63. meerschaum/api/models/__init__.py +23 -3
  64. meerschaum/api/models/_actions.py +22 -0
  65. meerschaum/api/models/_pipes.py +91 -7
  66. meerschaum/api/models/_tokens.py +81 -0
  67. meerschaum/api/resources/static/css/dash.css +16 -0
  68. meerschaum/api/resources/static/js/terminado.js +3 -0
  69. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  70. meerschaum/api/resources/templates/termpage.html +13 -0
  71. meerschaum/api/routes/__init__.py +1 -0
  72. meerschaum/api/routes/_actions.py +3 -4
  73. meerschaum/api/routes/_connectors.py +3 -7
  74. meerschaum/api/routes/_jobs.py +26 -35
  75. meerschaum/api/routes/_login.py +120 -15
  76. meerschaum/api/routes/_misc.py +5 -10
  77. meerschaum/api/routes/_pipes.py +178 -143
  78. meerschaum/api/routes/_plugins.py +38 -28
  79. meerschaum/api/routes/_tokens.py +236 -0
  80. meerschaum/api/routes/_users.py +47 -35
  81. meerschaum/api/routes/_version.py +3 -3
  82. meerschaum/api/routes/_webterm.py +3 -3
  83. meerschaum/config/__init__.py +100 -30
  84. meerschaum/config/_default.py +132 -64
  85. meerschaum/config/_edit.py +38 -32
  86. meerschaum/config/_formatting.py +2 -0
  87. meerschaum/config/_patch.py +10 -8
  88. meerschaum/config/_paths.py +133 -13
  89. meerschaum/config/_read_config.py +87 -36
  90. meerschaum/config/_sync.py +6 -3
  91. meerschaum/config/_version.py +1 -1
  92. meerschaum/config/environment.py +262 -0
  93. meerschaum/config/stack/__init__.py +37 -15
  94. meerschaum/config/static.py +18 -0
  95. meerschaum/connectors/_Connector.py +11 -6
  96. meerschaum/connectors/__init__.py +41 -22
  97. meerschaum/connectors/api/_APIConnector.py +34 -6
  98. meerschaum/connectors/api/_actions.py +2 -2
  99. meerschaum/connectors/api/_jobs.py +12 -1
  100. meerschaum/connectors/api/_login.py +33 -7
  101. meerschaum/connectors/api/_misc.py +2 -2
  102. meerschaum/connectors/api/_pipes.py +23 -32
  103. meerschaum/connectors/api/_plugins.py +2 -2
  104. meerschaum/connectors/api/_request.py +1 -1
  105. meerschaum/connectors/api/_tokens.py +146 -0
  106. meerschaum/connectors/api/_users.py +70 -58
  107. meerschaum/connectors/instance/_InstanceConnector.py +83 -0
  108. meerschaum/connectors/instance/__init__.py +10 -0
  109. meerschaum/connectors/instance/_pipes.py +442 -0
  110. meerschaum/connectors/instance/_plugins.py +159 -0
  111. meerschaum/connectors/instance/_tokens.py +317 -0
  112. meerschaum/connectors/instance/_users.py +188 -0
  113. meerschaum/connectors/parse.py +5 -2
  114. meerschaum/connectors/sql/_SQLConnector.py +22 -5
  115. meerschaum/connectors/sql/_cli.py +12 -11
  116. meerschaum/connectors/sql/_create_engine.py +12 -168
  117. meerschaum/connectors/sql/_fetch.py +2 -18
  118. meerschaum/connectors/sql/_pipes.py +295 -278
  119. meerschaum/connectors/sql/_plugins.py +29 -0
  120. meerschaum/connectors/sql/_sql.py +47 -22
  121. meerschaum/connectors/sql/_users.py +36 -2
  122. meerschaum/connectors/sql/tables/__init__.py +254 -122
  123. meerschaum/connectors/valkey/_ValkeyConnector.py +5 -7
  124. meerschaum/connectors/valkey/_pipes.py +60 -31
  125. meerschaum/connectors/valkey/_plugins.py +2 -26
  126. meerschaum/core/Pipe/__init__.py +115 -85
  127. meerschaum/core/Pipe/_attributes.py +425 -124
  128. meerschaum/core/Pipe/_bootstrap.py +54 -24
  129. meerschaum/core/Pipe/_cache.py +555 -0
  130. meerschaum/core/Pipe/_clear.py +0 -11
  131. meerschaum/core/Pipe/_data.py +96 -68
  132. meerschaum/core/Pipe/_deduplicate.py +0 -13
  133. meerschaum/core/Pipe/_delete.py +12 -21
  134. meerschaum/core/Pipe/_drop.py +11 -23
  135. meerschaum/core/Pipe/_dtypes.py +49 -19
  136. meerschaum/core/Pipe/_edit.py +14 -4
  137. meerschaum/core/Pipe/_fetch.py +1 -1
  138. meerschaum/core/Pipe/_index.py +8 -14
  139. meerschaum/core/Pipe/_show.py +5 -5
  140. meerschaum/core/Pipe/_sync.py +123 -204
  141. meerschaum/core/Pipe/_verify.py +4 -4
  142. meerschaum/{plugins → core/Plugin}/_Plugin.py +16 -12
  143. meerschaum/core/Plugin/__init__.py +1 -1
  144. meerschaum/core/Token/_Token.py +220 -0
  145. meerschaum/core/Token/__init__.py +12 -0
  146. meerschaum/core/User/_User.py +35 -10
  147. meerschaum/core/User/__init__.py +9 -1
  148. meerschaum/core/__init__.py +1 -0
  149. meerschaum/jobs/_Executor.py +88 -4
  150. meerschaum/jobs/_Job.py +149 -38
  151. meerschaum/jobs/__init__.py +3 -2
  152. meerschaum/jobs/systemd.py +8 -3
  153. meerschaum/models/__init__.py +35 -0
  154. meerschaum/models/pipes.py +247 -0
  155. meerschaum/models/tokens.py +38 -0
  156. meerschaum/models/users.py +26 -0
  157. meerschaum/plugins/__init__.py +301 -88
  158. meerschaum/plugins/bootstrap.py +510 -4
  159. meerschaum/utils/_get_pipes.py +97 -30
  160. meerschaum/utils/daemon/Daemon.py +199 -43
  161. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  162. meerschaum/utils/daemon/RotatingFile.py +63 -36
  163. meerschaum/utils/daemon/StdinFile.py +53 -13
  164. meerschaum/utils/daemon/__init__.py +47 -6
  165. meerschaum/utils/daemon/_names.py +6 -3
  166. meerschaum/utils/dataframe.py +480 -82
  167. meerschaum/utils/debug.py +49 -19
  168. meerschaum/utils/dtypes/__init__.py +478 -37
  169. meerschaum/utils/dtypes/sql.py +369 -29
  170. meerschaum/utils/formatting/__init__.py +5 -2
  171. meerschaum/utils/formatting/_jobs.py +1 -1
  172. meerschaum/utils/formatting/_pipes.py +52 -50
  173. meerschaum/utils/formatting/_pprint.py +1 -0
  174. meerschaum/utils/formatting/_shell.py +44 -18
  175. meerschaum/utils/misc.py +268 -186
  176. meerschaum/utils/packages/__init__.py +25 -40
  177. meerschaum/utils/packages/_packages.py +42 -34
  178. meerschaum/utils/pipes.py +213 -0
  179. meerschaum/utils/process.py +2 -2
  180. meerschaum/utils/prompt.py +175 -144
  181. meerschaum/utils/schedule.py +2 -1
  182. meerschaum/utils/sql.py +135 -49
  183. meerschaum/utils/threading.py +42 -0
  184. meerschaum/utils/typing.py +1 -4
  185. meerschaum/utils/venv/_Venv.py +2 -2
  186. meerschaum/utils/venv/__init__.py +7 -7
  187. meerschaum/utils/warnings.py +19 -13
  188. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/METADATA +94 -96
  189. meerschaum-3.0.0.dist-info/RECORD +289 -0
  190. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/WHEEL +1 -1
  191. meerschaum-3.0.0.dist-info/licenses/NOTICE +2 -0
  192. meerschaum/api/models/_interfaces.py +0 -15
  193. meerschaum/api/models/_locations.py +0 -15
  194. meerschaum/api/models/_metrics.py +0 -15
  195. meerschaum/config/_environment.py +0 -145
  196. meerschaum/config/static/__init__.py +0 -186
  197. meerschaum-2.9.4.dist-info/RECORD +0 -263
  198. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/entry_points.txt +0 -0
  199. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/licenses/LICENSE +0 -0
  200. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/top_level.txt +0 -0
  201. {meerschaum-2.9.4.dist-info → meerschaum-3.0.0.dist-info}/zip-safe +0 -0
@@ -14,7 +14,8 @@ from meerschaum.utils.typing import Any, Union, Optional, Tuple, List
14
14
 
15
15
 
16
16
  def prompt(
17
- question: str,
17
+ question: str = '',
18
+ *,
18
19
  icon: bool = True,
19
20
  default: Union[str, Tuple[str, str], None] = None,
20
21
  default_editable: Optional[str] = None,
@@ -22,6 +23,7 @@ def prompt(
22
23
  is_password: bool = False,
23
24
  wrap_lines: bool = True,
24
25
  noask: bool = False,
26
+ silent: bool = False,
25
27
  **kw: Any
26
28
  ) -> str:
27
29
  """
@@ -59,6 +61,9 @@ def prompt(
59
61
  noask: bool, default False
60
62
  If `True`, only print the question and return the default answer.
61
63
 
64
+ silent: bool, default False
65
+ If `True` do not print anything to the screen, but still block for input.
66
+
62
67
  Returns
63
68
  -------
64
69
  A `str` of the input provided by the user.
@@ -67,11 +72,25 @@ def prompt(
67
72
  from meerschaum.utils.packages import attempt_import
68
73
  from meerschaum.utils.formatting import ANSI, CHARSET, highlight_pipes, fill_ansi
69
74
  from meerschaum.config import get_config
70
- from meerschaum.utils.misc import filter_keywords
71
- from meerschaum.utils.daemon import running_in_daemon
75
+ from meerschaum.utils.misc import filter_keywords, remove_ansi
76
+ from meerschaum.utils.daemon import running_in_daemon, get_current_daemon
77
+ from meerschaum._internal.static import STATIC_CONFIG
78
+
79
+ original_kwargs = {
80
+ 'question': question,
81
+ 'icon': icon,
82
+ 'default': default,
83
+ 'default_editable': default_editable,
84
+ 'detect_password': detect_password,
85
+ 'is_password': is_password,
86
+ 'wrap_lines': wrap_lines,
87
+ 'noask': noask,
88
+ 'silent': silent,
89
+ **kw
90
+ }
91
+
72
92
  noask = check_noask(noask)
73
- if not noask:
74
- prompt_toolkit = attempt_import('prompt_toolkit')
93
+ prompt_toolkit = attempt_import('prompt_toolkit')
75
94
  question_config = get_config('formatting', 'question', patch=True)
76
95
 
77
96
  ### if a default is provided, append it to the question.
@@ -103,27 +122,50 @@ def prompt(
103
122
  question += first_line
104
123
  if len(other_lines) > 0:
105
124
  question += '\n' + other_lines
106
- question += ' '
125
+
126
+ if not remove_ansi(question).endswith(' '):
127
+ question += ' '
128
+
129
+ prompt_kwargs = {
130
+ 'message': prompt_toolkit.formatted_text.ANSI(question) if not silent else '',
131
+ 'wrap_lines': wrap_lines,
132
+ 'default': default_editable or '',
133
+ **filter_keywords(prompt_toolkit.prompt, **kw)
134
+ }
135
+
136
+ printed_question = False
107
137
 
108
138
  if not running_in_daemon():
109
- answer = (
110
- prompt_toolkit.prompt(
111
- prompt_toolkit.formatted_text.ANSI(question),
112
- wrap_lines=wrap_lines,
113
- default=default_editable or '',
114
- **filter_keywords(prompt_toolkit.prompt, **kw)
115
- ) if not noask else ''
116
- )
139
+ answer = prompt_toolkit.prompt(**prompt_kwargs) if not noask else ''
140
+ printed_question = True
117
141
  else:
118
- print(question, end='\n', flush=True)
142
+ import json
143
+ daemon = get_current_daemon()
144
+ print(STATIC_CONFIG['jobs']['flush_token'], end='', flush=True)
145
+ wrote_file = False
146
+ try:
147
+ with open(daemon.prompt_kwargs_file_path, 'w+', encoding='utf-8') as f:
148
+ json.dump(original_kwargs, f, separators=(',', ':'))
149
+ wrote_file = True
150
+ except Exception:
151
+ pass
152
+
153
+ if not silent and not wrote_file:
154
+ print(question, end='', flush=True)
155
+ print(STATIC_CONFIG['jobs']['flush_token'], end='', flush=True)
156
+ printed_question = True
157
+
119
158
  try:
120
159
  answer = input() if not noask else ''
121
160
  except EOFError:
122
161
  answer = ''
123
- if noask:
162
+
163
+ if noask and not silent and not printed_question:
124
164
  print(question)
165
+
125
166
  if answer == '' and default is not None:
126
167
  return default_answer
168
+
127
169
  return answer
128
170
 
129
171
 
@@ -207,7 +249,7 @@ def yes_no(
207
249
 
208
250
  def choose(
209
251
  question: str,
210
- choices: List[Union[str, Tuple[str, str]]],
252
+ choices: Union[List[str], List[Tuple[str, str]]],
211
253
  default: Union[str, List[str], None] = None,
212
254
  numeric: bool = True,
213
255
  multiple: bool = False,
@@ -270,8 +312,9 @@ def choose(
270
312
  noask = check_noask(noask)
271
313
 
272
314
  ### Handle empty choices.
273
- if len(choices) == 0:
274
- _warn(f"No available choices. Returning default value '{default}'.", stacklevel=3)
315
+ if not choices:
316
+ if warn:
317
+ _warn(f"No available choices. Returning default value '{default}'.", stacklevel=3)
275
318
  return default
276
319
 
277
320
  ### If the default case is to include multiple answers, allow for multiple inputs.
@@ -279,18 +322,25 @@ def choose(
279
322
  multiple = True
280
323
 
281
324
  choices_indices = {}
282
- for i, c in enumerate(choices):
325
+ for i, c in enumerate(choices, start=1):
283
326
  if isinstance(c, tuple):
284
327
  i, c = c
285
328
  choices_indices[i] = c
286
329
 
330
+ choices_values_indices = {v: k for k, v in choices_indices.items()}
331
+ ordered_keys = list(choices_indices.keys())
332
+ numeric_map = {str(i): key for i, key in enumerate(ordered_keys, 1)}
333
+
287
334
  def _enforce_default(d):
288
- if d is not None and d not in choices and d not in choices_indices and warn:
289
- _warn(
290
- f"Default choice '{default}' is not contained in the choices {choices}. "
291
- + "Setting numeric = False.",
292
- stacklevel = 3
293
- )
335
+ if d is None:
336
+ return True
337
+ if d not in choices_values_indices and d not in choices_indices:
338
+ if warn:
339
+ _warn(
340
+ f"Default choice '{d}' is not contained in the choices. "
341
+ + "Setting numeric = False.",
342
+ stacklevel=3
343
+ )
294
344
  return False
295
345
  return True
296
346
 
@@ -300,161 +350,141 @@ def choose(
300
350
  numeric = False
301
351
  break
302
352
 
303
- _default = default
304
- _choices = list(choices_indices.values())
353
+ _choices = (
354
+ [str(k) for k in choices_indices] if numeric
355
+ else list(choices_indices.values())
356
+ )
305
357
  if multiple:
306
358
  question += f"\n Enter your choices, separated by '{delimiter}'.\n"
307
359
 
308
360
  altered_choices = {}
309
- altered_indices = {}
310
- altered_default_indices = {}
311
- delim_replacement = '_' if delimiter != '_' else '-'
312
- can_strip_start_spaces, can_strip_end_spaces = True, True
313
- for i, c in choices_indices.items():
314
- if isinstance(c, tuple):
315
- key, c = c
316
- if can_strip_start_spaces and c.startswith(' '):
317
- can_strip_start_spaces = False
318
- if can_strip_end_spaces and c.endswith(' '):
319
- can_strip_end_spaces = False
320
-
321
- if multiple:
322
- ### Check if the defaults have the delimiter.
323
- for i, d in enumerate(default if isinstance(default, list) else [default]):
324
- if d is None or delimiter not in d:
325
- continue
326
- new_d = d.replace(delimiter, delim_replacement)
327
- altered_choices[new_d] = d
328
- altered_default_indices[i] = new_d
329
- for i, new_d in altered_default_indices.items():
330
- if not isinstance(default, list):
331
- default = new_d
332
- break
333
- default[i] = new_d
334
-
361
+ if multiple and not numeric:
362
+ delim_replacement = '_' if delimiter != '_' else '-'
335
363
  ### Check if the choices have the delimiter.
336
364
  for i, c in choices_indices.items():
337
- if delimiter in c and not numeric and warn:
365
+ if delimiter not in c:
366
+ continue
367
+ if warn:
338
368
  _warn(
339
369
  f"The delimiter '{delimiter}' is contained within choice '{c}'.\n"
340
370
  + f"Replacing the string '{delimiter}' with '{delim_replacement}' in "
341
371
  + "the choice for correctly parsing input (will be replaced upon returning the prompt).",
342
- stacklevel = 3,
372
+ stacklevel=3,
343
373
  )
344
- new_c = c.replace(delimiter, delim_replacement)
345
- altered_choices[new_c] = c
346
- altered_indices[i] = new_c
347
- for i, new_c in altered_indices.items():
374
+ new_c = c.replace(delimiter, delim_replacement)
375
+ altered_choices[new_c] = c
348
376
  choices_indices[i] = new_c
349
- default = delimiter.join(default) if isinstance(default, list) else default
350
377
 
351
378
  question_options = []
379
+ default_tuple = None
352
380
  if numeric:
353
- _choices = [str(i + 1) for i, c in enumerate(choices)]
354
- _default = ''
381
+ _default_prompt_str = ''
355
382
  if default is not None:
356
- for d in (default.split(delimiter) if multiple else [default]):
357
- if d not in choices and d in choices_indices:
358
- d_index = d
359
- d_value = choices_indices[d]
360
- for _i, _option in enumerate(choices):
361
- if (
362
- isinstance(_option, tuple) and (
363
- _option[1] == d_value
364
- or
365
- _option[0] == d_index
366
- )
367
- ) or d_index == _i:
368
- d = _option
369
-
370
- _d = str(choices.index(d) + 1)
371
- _default += _d + delimiter
372
- _default = _default[:-1 * len(delimiter)]
373
- # question += '\n'
383
+ default_list = default if isinstance(default, list) else [default]
384
+ if multiple and isinstance(default, str):
385
+ default_list = default.split(delimiter)
386
+
387
+ _default_indices = []
388
+ for d in default_list:
389
+ key = None
390
+ if d in choices_values_indices: # is a value
391
+ key = choices_values_indices[d]
392
+ elif d in choices_indices: # is an index
393
+ key = d
394
+
395
+ if key in ordered_keys:
396
+ _default_indices.append(str(ordered_keys.index(key) + 1))
397
+
398
+ _default_prompt_str = delimiter.join(_default_indices)
399
+
374
400
  choices_digits = len(str(len(choices)))
375
- for i, c in enumerate(choices_indices.values()):
401
+ for choice_ix, c in enumerate(choices_indices.values(), start=1):
376
402
  question_options.append(
377
- f" {i + 1}. "
378
- + (" " * (choices_digits - len(str(i + 1))))
403
+ f" {choice_ix}. "
404
+ + (" " * (choices_digits - len(str(choice_ix))))
379
405
  + f"{c}\n"
380
406
  )
381
- default_tuple = (_default, default) if default is not None else None
407
+ default_tuple = (_default_prompt_str, default) if default is not None else None
382
408
  else:
383
409
  default_tuple = default
384
- # question += '\n'
385
410
  for c in choices_indices.values():
386
- question_options.append(f"{c}\n")
411
+ question_options.append(f"{c}\n")
387
412
 
388
413
  if 'completer' not in kw:
389
- WordCompleter = attempt_import('prompt_toolkit.completion').WordCompleter
390
- kw['completer'] = WordCompleter(choices_indices.values(), sentence=True)
414
+ WordCompleter = attempt_import('prompt_toolkit.completion', lazy=False).WordCompleter
415
+ kw['completer'] = WordCompleter(
416
+ [str(v) for v in choices_indices.values()] + [str(i) for i in choices_indices],
417
+ sentence=True,
418
+ )
391
419
 
392
- valid = False
393
- while not valid:
420
+ answers = []
421
+ while not answers:
394
422
  print_options(question_options, header='')
395
423
  answer = prompt(
396
424
  question,
397
- icon = icon,
398
- default = default_tuple,
399
- noask = noask,
425
+ icon=icon,
426
+ default=default_tuple,
427
+ noask=noask,
400
428
  **kw
401
429
  )
402
- ### Split along the delimiter.
403
- _answers = [answer] if not multiple else [a for a in answer.split(delimiter)]
404
-
405
- ### Remove trailing spaces if possible.
406
- _answers = [(_a.rstrip(' ') if can_strip_end_spaces else _a) for _a in _answers]
407
-
408
- ### Remove leading spaces if possible.
409
- _answers = [(_a.lstrip(' ') if can_strip_start_spaces else _a) for _a in _answers]
410
-
411
- ### Remove empty strings.
412
- _answers = [_a for _a in _answers if _a]
413
-
414
- if multiple and len(_answers) == 0:
415
- _answers = default_tuple if isinstance(default_tuple, list) else [default_tuple]
416
- answers = [altered_choices.get(a, a) for a in _answers]
417
-
418
- valid = (len(answers) > 1 or not (len(answers) == 1 and answers[0] is None))
419
- for a in answers:
420
- if (
421
- a not in {_original for _new, _original in altered_choices.items()}
422
- and a not in _choices
423
- and a != default
424
- and not noask
425
- ):
426
- valid = False
427
- break
428
- if valid:
429
- break
430
- if warn:
431
- _warn("Please pick a valid choice.", stack=False)
430
+ if not answer and default is not None:
431
+ answer = default if isinstance(default, str) else delimiter.join(default)
432
+
433
+ if not answer:
434
+ if warn:
435
+ _warn("Please pick a valid choice.", stack=False)
436
+ continue
437
+
438
+ _answers = [answer] if not multiple else [a.strip() for a in answer.split(delimiter)]
439
+ _answers = [a for a in _answers if a]
440
+
441
+ if numeric:
442
+ _raw_answers = list(_answers)
443
+ _answers = []
444
+ for _a in _raw_answers:
445
+ if _a in choices_values_indices:
446
+ _answers.append(str(choices_values_indices[_a]))
447
+ elif _a in numeric_map:
448
+ _answers.append(str(numeric_map[_a]))
449
+ else:
450
+ _answers.append(_a)
451
+
452
+ _processed_answers = [altered_choices.get(a, a) for a in _answers]
453
+
454
+ valid_answers = []
455
+ for a in _processed_answers:
456
+ if a in _choices:
457
+ valid_answers.append(a)
458
+
459
+ if len(valid_answers) != len(_processed_answers):
460
+ if warn:
461
+ _warn("Please pick a valid choice.", stack=False)
462
+ continue
463
+ answers = valid_answers
464
+
465
+ def get_key(key_str):
466
+ try:
467
+ return int(key_str)
468
+ except (ValueError, TypeError):
469
+ return key_str
432
470
 
433
471
  if not multiple:
472
+ answer = answers[0]
434
473
  if not numeric:
435
- return answer
436
- try:
437
- _answer = choices[int(answer) - 1]
438
- if as_indices and isinstance(_answer, tuple):
439
- return _answer[0]
440
- return _answer
441
- except Exception:
442
- _warn(f"Could not cast answer '{answer}' to an integer.", stacklevel=3)
474
+ return choices_values_indices.get(answer, answer) if as_indices else answer
475
+
476
+ key = get_key(answer)
477
+ return key if as_indices else choices_indices[key]
443
478
 
444
479
  if not numeric:
445
- return answers
480
+ return [choices_values_indices.get(a, a) for a in answers] if as_indices else answers
446
481
 
447
- _answers = []
482
+ final_answers = []
448
483
  for a in answers:
449
- try:
450
- _answer = choices[int(a) - 1]
451
- _answer_to_return = altered_choices.get(_answer, _answer)
452
- if isinstance(_answer_to_return, tuple) and as_indices:
453
- _answer_to_return = _answer_to_return[0]
454
- _answers.append(_answer_to_return)
455
- except Exception:
456
- _warn(f"Could not cast answer '{a}' to an integer.", stacklevel=3)
457
- return _answers
484
+ key = get_key(a)
485
+ final_answers.append(key if as_indices else choices_indices[key])
486
+ return final_answers
487
+
458
488
 
459
489
 
460
490
  def get_password(
@@ -564,10 +594,11 @@ def check_noask(noask: bool = False) -> bool:
564
594
  """
565
595
  Flip `noask` to `True` if `MRSM_NOASK` is set.
566
596
  """
567
- from meerschaum.config.static import STATIC_CONFIG
597
+ from meerschaum._internal.static import STATIC_CONFIG
568
598
  NOASK = STATIC_CONFIG['environment']['noask']
569
599
  if noask:
570
600
  return True
601
+
571
602
  return (
572
603
  os.environ.get(NOASK, 'false').lower()
573
604
  in ('1', 'true')
@@ -14,6 +14,7 @@ import meerschaum as mrsm
14
14
  from meerschaum.utils.typing import Callable, Any, Optional, List, Dict
15
15
  from meerschaum.utils.warnings import warn, error
16
16
 
17
+
17
18
  STARTING_KEYWORD: str = 'starting'
18
19
  INTERVAL_UNITS: List[str] = ['months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'years']
19
20
  FREQUENCY_ALIASES: Dict[str, str] = {
@@ -292,7 +293,7 @@ def parse_start_time(schedule: str, now: Optional[datetime] = None) -> datetime:
292
293
  >>> parse_start_time('hourly starting 00:30')
293
294
  datetime.datetime(2024, 5, 13, 0, 30, tzinfo=datetime.timezone.utc)
294
295
  """
295
- from meerschaum.utils.misc import round_time
296
+ from meerschaum.utils.dtypes import round_time
296
297
  dateutil_parser = mrsm.attempt_import('dateutil.parser')
297
298
  starting_parts = schedule.split(STARTING_KEYWORD)
298
299
  starting_str = ('now' if len(starting_parts) == 1 else starting_parts[-1]).strip()