meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc8__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 (117) hide show
  1. meerschaum/_internal/arguments/_parser.py +14 -2
  2. meerschaum/_internal/cli/__init__.py +6 -0
  3. meerschaum/_internal/cli/daemons.py +103 -0
  4. meerschaum/_internal/cli/entry.py +220 -0
  5. meerschaum/_internal/cli/workers.py +435 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +115 -24
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +4 -1
  11. meerschaum/_internal/term/TermPageHandler.py +1 -2
  12. meerschaum/_internal/term/__init__.py +40 -6
  13. meerschaum/_internal/term/tools.py +33 -8
  14. meerschaum/actions/__init__.py +6 -4
  15. meerschaum/actions/api.py +39 -11
  16. meerschaum/actions/attach.py +1 -0
  17. meerschaum/actions/delete.py +4 -2
  18. meerschaum/actions/edit.py +27 -8
  19. meerschaum/actions/login.py +8 -8
  20. meerschaum/actions/register.py +13 -7
  21. meerschaum/actions/reload.py +22 -5
  22. meerschaum/actions/restart.py +14 -0
  23. meerschaum/actions/show.py +69 -4
  24. meerschaum/actions/start.py +135 -14
  25. meerschaum/actions/stop.py +36 -3
  26. meerschaum/actions/sync.py +6 -1
  27. meerschaum/api/__init__.py +35 -13
  28. meerschaum/api/_events.py +2 -2
  29. meerschaum/api/_oauth2.py +47 -4
  30. meerschaum/api/dash/callbacks/dashboard.py +29 -0
  31. meerschaum/api/dash/callbacks/jobs.py +3 -2
  32. meerschaum/api/dash/callbacks/login.py +10 -1
  33. meerschaum/api/dash/callbacks/register.py +9 -2
  34. meerschaum/api/dash/pages/login.py +2 -2
  35. meerschaum/api/dash/pipes.py +72 -36
  36. meerschaum/api/dash/webterm.py +14 -6
  37. meerschaum/api/models/_pipes.py +7 -1
  38. meerschaum/api/resources/static/js/terminado.js +3 -0
  39. meerschaum/api/resources/static/js/xterm-addon-unicode11.js +2 -0
  40. meerschaum/api/resources/templates/termpage.html +1 -0
  41. meerschaum/api/routes/_jobs.py +23 -11
  42. meerschaum/api/routes/_login.py +73 -5
  43. meerschaum/api/routes/_pipes.py +6 -4
  44. meerschaum/api/routes/_webterm.py +3 -3
  45. meerschaum/config/__init__.py +60 -13
  46. meerschaum/config/_default.py +89 -61
  47. meerschaum/config/_edit.py +10 -8
  48. meerschaum/config/_formatting.py +2 -0
  49. meerschaum/config/_patch.py +4 -2
  50. meerschaum/config/_paths.py +127 -12
  51. meerschaum/config/_read_config.py +32 -12
  52. meerschaum/config/_version.py +1 -1
  53. meerschaum/config/environment.py +262 -0
  54. meerschaum/config/stack/__init__.py +7 -5
  55. meerschaum/connectors/_Connector.py +1 -2
  56. meerschaum/connectors/__init__.py +37 -2
  57. meerschaum/connectors/api/_APIConnector.py +1 -1
  58. meerschaum/connectors/api/_jobs.py +11 -0
  59. meerschaum/connectors/api/_pipes.py +7 -1
  60. meerschaum/connectors/instance/_plugins.py +9 -1
  61. meerschaum/connectors/instance/_tokens.py +20 -3
  62. meerschaum/connectors/instance/_users.py +8 -1
  63. meerschaum/connectors/parse.py +1 -1
  64. meerschaum/connectors/sql/_create_engine.py +3 -0
  65. meerschaum/connectors/sql/_pipes.py +93 -79
  66. meerschaum/connectors/sql/_users.py +8 -1
  67. meerschaum/connectors/valkey/_ValkeyConnector.py +3 -3
  68. meerschaum/connectors/valkey/_pipes.py +7 -5
  69. meerschaum/core/Pipe/__init__.py +45 -71
  70. meerschaum/core/Pipe/_attributes.py +66 -90
  71. meerschaum/core/Pipe/_cache.py +555 -0
  72. meerschaum/core/Pipe/_clear.py +0 -11
  73. meerschaum/core/Pipe/_data.py +0 -50
  74. meerschaum/core/Pipe/_deduplicate.py +0 -13
  75. meerschaum/core/Pipe/_delete.py +12 -21
  76. meerschaum/core/Pipe/_drop.py +11 -23
  77. meerschaum/core/Pipe/_dtypes.py +1 -1
  78. meerschaum/core/Pipe/_index.py +8 -14
  79. meerschaum/core/Pipe/_sync.py +12 -18
  80. meerschaum/core/Plugin/_Plugin.py +7 -1
  81. meerschaum/core/Token/_Token.py +1 -1
  82. meerschaum/core/User/_User.py +1 -2
  83. meerschaum/jobs/_Executor.py +88 -4
  84. meerschaum/jobs/_Job.py +146 -36
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +197 -42
  88. meerschaum/utils/daemon/FileDescriptorInterceptor.py +0 -1
  89. meerschaum/utils/daemon/RotatingFile.py +63 -36
  90. meerschaum/utils/daemon/StdinFile.py +53 -13
  91. meerschaum/utils/daemon/__init__.py +18 -5
  92. meerschaum/utils/daemon/_names.py +6 -3
  93. meerschaum/utils/debug.py +34 -4
  94. meerschaum/utils/dtypes/__init__.py +5 -1
  95. meerschaum/utils/formatting/__init__.py +4 -1
  96. meerschaum/utils/formatting/_jobs.py +1 -1
  97. meerschaum/utils/formatting/_pipes.py +47 -46
  98. meerschaum/utils/formatting/_shell.py +33 -9
  99. meerschaum/utils/misc.py +22 -38
  100. meerschaum/utils/packages/__init__.py +15 -13
  101. meerschaum/utils/packages/_packages.py +1 -0
  102. meerschaum/utils/pipes.py +33 -5
  103. meerschaum/utils/process.py +1 -1
  104. meerschaum/utils/prompt.py +172 -143
  105. meerschaum/utils/sql.py +12 -2
  106. meerschaum/utils/threading.py +42 -0
  107. meerschaum/utils/venv/__init__.py +2 -0
  108. meerschaum/utils/warnings.py +19 -13
  109. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc8.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,163 +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
  )
430
+ if not answer and default is not None:
431
+ answer = default if isinstance(default, str) else delimiter.join(default)
432
+
402
433
  if not answer:
434
+ if warn:
435
+ _warn("Please pick a valid choice.", stack=False)
403
436
  continue
404
- ### Split along the delimiter.
405
- _answers = [answer] if not multiple else [a for a in answer.split(delimiter)]
406
-
407
- ### Remove trailing spaces if possible.
408
- _answers = [(_a.rstrip(' ') if can_strip_end_spaces else _a) for _a in _answers]
409
-
410
- ### Remove leading spaces if possible.
411
- _answers = [(_a.lstrip(' ') if can_strip_start_spaces else _a) for _a in _answers]
412
-
413
- ### Remove empty strings.
414
- _answers = [_a for _a in _answers if _a]
415
-
416
- if multiple and len(_answers) == 0:
417
- _answers = default_tuple if isinstance(default_tuple, list) else [default_tuple]
418
- answers = [altered_choices.get(a, a) for a in _answers]
419
-
420
- valid = (len(answers) > 1 or not (len(answers) == 1 and answers[0] is None))
421
- for a in answers:
422
- if (
423
- a not in {_original for _new, _original in altered_choices.items()}
424
- and a not in _choices
425
- and a != default
426
- and not noask
427
- ):
428
- valid = False
429
- break
430
- if valid:
431
- break
432
- if warn:
433
- _warn("Please pick a valid choice.", stack=False)
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
434
470
 
435
471
  if not multiple:
472
+ answer = answers[0]
436
473
  if not numeric:
437
- return answer
438
- try:
439
- _answer = choices[int(answer) - 1]
440
- if as_indices and isinstance(_answer, tuple):
441
- return _answer[0]
442
- return _answer
443
- except Exception:
444
- _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]
445
478
 
446
479
  if not numeric:
447
- return answers
480
+ return [choices_values_indices.get(a, a) for a in answers] if as_indices else answers
448
481
 
449
- _answers = []
482
+ final_answers = []
450
483
  for a in answers:
451
- try:
452
- _answer = choices[int(a) - 1]
453
- _answer_to_return = altered_choices.get(_answer, _answer)
454
- if isinstance(_answer_to_return, tuple) and as_indices:
455
- _answer_to_return = _answer_to_return[0]
456
- _answers.append(_answer_to_return)
457
- except Exception:
458
- _warn(f"Could not cast answer '{a}' to an integer.", stacklevel=3)
459
- return _answers
484
+ key = get_key(a)
485
+ final_answers.append(key if as_indices else choices_indices[key])
486
+ return final_answers
487
+
460
488
 
461
489
 
462
490
  def get_password(
@@ -570,6 +598,7 @@ def check_noask(noask: bool = False) -> bool:
570
598
  NOASK = STATIC_CONFIG['environment']['noask']
571
599
  if noask:
572
600
  return True
601
+
573
602
  return (
574
603
  os.environ.get(NOASK, 'false').lower()
575
604
  in ('1', 'true')
meerschaum/utils/sql.py CHANGED
@@ -1586,6 +1586,8 @@ def get_update_queries(
1586
1586
  datetime_col: Optional[str] = None,
1587
1587
  schema: Optional[str] = None,
1588
1588
  patch_schema: Optional[str] = None,
1589
+ target_cols_types: Optional[Dict[str, str]] = None,
1590
+ patch_cols_types: Optional[Dict[str, str]] = None,
1589
1591
  identity_insert: bool = False,
1590
1592
  null_indices: bool = True,
1591
1593
  cast_columns: bool = True,
@@ -1626,6 +1628,14 @@ def get_update_queries(
1626
1628
  If provided, use this schema when quoting the patch table.
1627
1629
  Defaults to `schema`.
1628
1630
 
1631
+ target_cols_types: Optional[Dict[str, Any]], default None
1632
+ If provided, use these as the columns-types dictionary for the target table.
1633
+ Default will infer from the database context.
1634
+
1635
+ patch_cols_types: Optional[Dict[str, Any]], default None
1636
+ If provided, use these as the columns-types dictionary for the target table.
1637
+ Default will infer from the database context.
1638
+
1629
1639
  identity_insert: bool, default False
1630
1640
  If `True`, include `SET IDENTITY_INSERT` queries before and after the update queries.
1631
1641
  Only applies for MSSQL upserts.
@@ -1672,14 +1682,14 @@ def get_update_queries(
1672
1682
  flavor=flavor,
1673
1683
  schema=schema,
1674
1684
  debug=debug,
1675
- )
1685
+ ) if not target_cols_types else target_cols_types
1676
1686
  patch_table_columns = get_table_cols_types(
1677
1687
  patch,
1678
1688
  connectable,
1679
1689
  flavor=flavor,
1680
1690
  schema=patch_schema,
1681
1691
  debug=debug,
1682
- )
1692
+ ) if not patch_cols_types else patch_cols_types
1683
1693
 
1684
1694
  patch_cols_str = ', '.join(
1685
1695
  [
@@ -10,6 +10,9 @@ from __future__ import annotations
10
10
  from meerschaum.utils.typing import Optional
11
11
 
12
12
  import threading
13
+ import ctypes
14
+ import signal
15
+
13
16
  Lock = threading.Lock
14
17
  RLock = threading.RLock
15
18
  Event = threading.Event
@@ -54,6 +57,45 @@ class Thread(threading.Thread):
54
57
  """Set the return to the result of the target."""
55
58
  self._return = self._target(*self._args, **self._kwargs)
56
59
 
60
+ def send_signal(self, signalnum):
61
+ """
62
+ Send a signal to the thread.
63
+ """
64
+ if not self.is_alive():
65
+ return
66
+
67
+ if signalnum == signal.SIGINT:
68
+ self.raise_exception(KeyboardInterrupt())
69
+ elif signalnum == signal.SIGTERM:
70
+ self.raise_exception(SystemExit())
71
+ else:
72
+ signal.pthread_kill(self.ident, signalnum)
73
+
74
+ def raise_exception(self, exc: BaseException):
75
+ """
76
+ Raise an exception in the thread.
77
+
78
+ This uses a CPython-specific implementation and is not guaranteed to be stable.
79
+ It may also be deprecated in future Python versions.
80
+ """
81
+ if not self.is_alive():
82
+ return
83
+
84
+ if not hasattr(ctypes.pythonapi, 'PyThreadState_SetAsyncExc'):
85
+ return
86
+
87
+ exc_class = exc if isinstance(exc, type) else type(exc)
88
+
89
+ ident = self.ident
90
+ if ident is None:
91
+ return
92
+
93
+ ret = ctypes.pythonapi.PyThreadState_SetAsyncExc(
94
+ ctypes.c_ulong(ident),
95
+ ctypes.py_object(exc_class)
96
+ )
97
+ if ret > 1:
98
+ ctypes.pythonapi.PyThreadState_SetAsyncExc(ident, 0)
57
99
 
58
100
  class Worker(threading.Thread):
59
101
  """Wrapper for `threading.Thread` for working with `queue.Queue` objects."""
@@ -8,6 +8,7 @@ Manage virtual environments.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import sys
11
12
  import pathlib
12
13
 
13
14
  from meerschaum.utils.typing import Optional, Union, Dict, List, Tuple
@@ -659,6 +660,7 @@ def venv_exec(
659
660
  cmd_list,
660
661
  stdout=stdout,
661
662
  stderr=stderr,
663
+ stdin=sys.stdin,
662
664
  env=env,
663
665
  **group_kwargs
664
666
  )
@@ -100,11 +100,11 @@ def warn(*args, stacklevel=2, stack=True, color: bool = True, **kw) -> None:
100
100
 
101
101
 
102
102
  def exception_with_traceback(
103
- message: str,
104
- exception_class = Exception,
105
- stacklevel = 1,
106
- tb_type = 'single'
107
- ):
103
+ message: str,
104
+ exception_class = Exception,
105
+ stacklevel = 1,
106
+ tb_type = 'single'
107
+ ):
108
108
  """Traceback construction help found here:
109
109
  https://stackoverflow.com/questions/27138440/how-to-create-a-traceback-object
110
110
  """
@@ -165,12 +165,16 @@ def error(
165
165
  Raise an exception with supressed traceback.
166
166
  """
167
167
  from meerschaum.utils.formatting import (
168
- CHARSET, ANSI, get_console, fill_ansi, highlight_pipes, _init
168
+ CHARSET,
169
+ ANSI,
170
+ get_console,
171
+ fill_ansi,
172
+ highlight_pipes,
173
+ _init,
169
174
  )
170
175
  from meerschaum.utils.packages import import_rich, attempt_import
171
176
  from meerschaum.config import get_config
172
- import types, inspect
173
- rich = import_rich()
177
+ _ = import_rich()
174
178
  rich_traceback = attempt_import('rich.traceback')
175
179
  error_config = get_config('formatting', 'errors', patch=True)
176
180
  message = ' ' + error_config[CHARSET]['icon'] + ' ' + str(message)
@@ -186,23 +190,25 @@ def error(
186
190
  exception_class, exception, exception.__traceback__
187
191
  )
188
192
  rtb = rich_traceback.Traceback(trace)
189
- except Exception as e:
193
+ except Exception:
190
194
  trace, rtb = None, None
191
195
  if trace is None or rtb is None:
192
196
  nopretty = True
193
197
  if not nopretty and stack:
194
198
  if get_console() is not None:
195
199
  get_console().print(rtb)
196
- frame = sys._getframe(len(inspect.stack()) - 1)
197
200
  if raise_:
198
201
  raise color_exception
199
202
 
200
203
 
201
204
  def info(message: str, icon: bool = True, **kw):
202
205
  """Print an informative message."""
203
- from meerschaum.utils.packages import import_rich, attempt_import
204
206
  from meerschaum.utils.formatting import (
205
- CHARSET, ANSI, highlight_pipes, fill_ansi, _init
207
+ CHARSET,
208
+ ANSI,
209
+ highlight_pipes,
210
+ fill_ansi,
211
+ _init,
206
212
  )
207
213
  from meerschaum.config import get_config
208
214
  info_config = get_config('formatting', 'info', patch=True)
@@ -215,4 +221,4 @@ def info(message: str, icon: bool = True, **kw):
215
221
  message = fill_ansi(lines[0], **info_config['ansi']['rich']) + (
216
222
  '\n' + '\n'.join(lines[1:]) if len(lines) > 1 else ''
217
223
  )
218
- print(message)
224
+ print(message, flush=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meerschaum
3
- Version: 3.0.0rc4
3
+ Version: 3.0.0rc8
4
4
  Summary: Sync Time-Series Pipes with Meerschaum
5
5
  Author-email: Bennett Meares <bennett.meares@gmail.com>
6
6
  Maintainer-email: Bennett Meares <bennett.meares@gmail.com>
@@ -192,6 +192,7 @@ Requires-Dist: python-multipart>=0.0.20; extra == "api"
192
192
  Requires-Dist: httpx>=0.28.1; extra == "api"
193
193
  Requires-Dist: httpcore>=1.0.9; extra == "api"
194
194
  Requires-Dist: valkey>=6.1.0; extra == "api"
195
+ Requires-Dist: python-jose>=3.5.0; extra == "api"
195
196
  Requires-Dist: numpy>=2.3.1; extra == "api"
196
197
  Requires-Dist: pandas[parquet]>=2.3.1; extra == "api"
197
198
  Requires-Dist: pyarrow>=20.0.0; extra == "api"
@@ -337,6 +338,7 @@ Requires-Dist: python-multipart>=0.0.20; extra == "full"
337
338
  Requires-Dist: httpx>=0.28.1; extra == "full"
338
339
  Requires-Dist: httpcore>=1.0.9; extra == "full"
339
340
  Requires-Dist: valkey>=6.1.0; extra == "full"
341
+ Requires-Dist: python-jose>=3.5.0; extra == "full"
340
342
  Dynamic: license-file
341
343
  Dynamic: provides-extra
342
344