meerschaum 2.2.4__py3-none-any.whl → 2.2.5.dev2__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.
- meerschaum/_internal/arguments/_parse_arguments.py +23 -14
- meerschaum/_internal/arguments/_parser.py +4 -1
- meerschaum/_internal/entry.py +2 -4
- meerschaum/_internal/shell/Shell.py +0 -3
- meerschaum/actions/__init__.py +5 -1
- meerschaum/actions/backup.py +43 -0
- meerschaum/actions/bootstrap.py +32 -7
- meerschaum/actions/delete.py +62 -0
- meerschaum/actions/edit.py +98 -15
- meerschaum/actions/python.py +44 -2
- meerschaum/actions/show.py +26 -0
- meerschaum/actions/uninstall.py +24 -29
- meerschaum/api/_oauth2.py +17 -0
- meerschaum/api/routes/_login.py +23 -7
- meerschaum/config/__init__.py +16 -6
- meerschaum/config/_edit.py +1 -1
- meerschaum/config/_paths.py +3 -0
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +3 -1
- meerschaum/core/Pipe/_fetch.py +25 -21
- meerschaum/core/Pipe/_sync.py +89 -59
- meerschaum/plugins/bootstrap.py +333 -0
- meerschaum/utils/daemon/Daemon.py +14 -3
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +21 -14
- meerschaum/utils/daemon/RotatingFile.py +21 -18
- meerschaum/utils/formatting/__init__.py +22 -10
- meerschaum/utils/packages/_packages.py +1 -1
- meerschaum/utils/prompt.py +64 -21
- meerschaum/utils/yaml.py +32 -1
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/METADATA +5 -2
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/RECORD +37 -35
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/WHEEL +1 -1
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/LICENSE +0 -0
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/NOTICE +0 -0
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/top_level.txt +0 -0
- {meerschaum-2.2.4.dist-info → meerschaum-2.2.5.dev2.dist-info}/zip-safe +0 -0
@@ -0,0 +1,333 @@
|
|
1
|
+
#! /usr/bin/env python3
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
# vim:fenc=utf-8
|
4
|
+
|
5
|
+
"""
|
6
|
+
Define the bootstrapping wizard for creating plugins.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import re
|
10
|
+
import pathlib
|
11
|
+
import meerschaum as mrsm
|
12
|
+
from meerschaum.utils.typing import Any, SuccessTuple, Dict, List
|
13
|
+
from meerschaum.utils.warnings import warn, info
|
14
|
+
from meerschaum.utils.prompt import prompt, choose, yes_no
|
15
|
+
from meerschaum.utils.formatting._shell import clear_screen
|
16
|
+
|
17
|
+
FEATURE_CHOICES: Dict[str, str] = {
|
18
|
+
'fetch' : 'Fetch data\n (e.g. extracting from a remote API)\n',
|
19
|
+
'connector': 'Custom connector\n (e.g. manage credentials)\n',
|
20
|
+
'action' : 'New actions\n (e.g. `mrsm sing song`)\n',
|
21
|
+
'api' : 'New API endpoints\n (e.g. `POST /my/new/endpoint`)\n',
|
22
|
+
'web' : 'New web console page\n (e.g. `/dash/my-web-app`)\n',
|
23
|
+
}
|
24
|
+
|
25
|
+
IMPORTS_LINES: Dict[str, str] = {
|
26
|
+
'stdlib': (
|
27
|
+
"from datetime import datetime, timedelta, timezone\n"
|
28
|
+
),
|
29
|
+
'default': (
|
30
|
+
"import meerschaum as mrsm\n"
|
31
|
+
"from meerschaum.config import get_plugin_config, write_plugin_config\n"
|
32
|
+
),
|
33
|
+
'connector': (
|
34
|
+
"from meerschaum.connectors import Connector, make_connector\n"
|
35
|
+
),
|
36
|
+
'action': (
|
37
|
+
"from meerschaum.actions import make_action\n"
|
38
|
+
),
|
39
|
+
'api': (
|
40
|
+
"from meerschaum.plugins import api_plugin\n"
|
41
|
+
),
|
42
|
+
'web': (
|
43
|
+
"from meerschaum.plugins import web_page, dash_plugin\n"
|
44
|
+
),
|
45
|
+
'api+web': (
|
46
|
+
"from meerschaum.plugins import api_plugin, web_page, dash_plugin\n"
|
47
|
+
),
|
48
|
+
}
|
49
|
+
|
50
|
+
### TODO: Add feature for custom connectors.
|
51
|
+
FEATURE_LINES: Dict[str, str] = {
|
52
|
+
'header': (
|
53
|
+
"#! /usr/bin/env python3\n"
|
54
|
+
"# -*- coding: utf-8 -*-\n\n"
|
55
|
+
"\"\"\"\n"
|
56
|
+
"Implement the plugin '{plugin_name}'.\n\n"
|
57
|
+
"See the Writing Plugins guide for more information:\n"
|
58
|
+
"https://meerschaum.io/reference/plugins/writing-plugins/\n"
|
59
|
+
"\"\"\"\n\n"
|
60
|
+
),
|
61
|
+
'default': (
|
62
|
+
"__version__ = '0.0.1'\n"
|
63
|
+
"\n# Add any dependencies to `required` (similar to `requirements.txt`).\n"
|
64
|
+
"required: list[str] = []\n\n\n"
|
65
|
+
),
|
66
|
+
'setup': (
|
67
|
+
"def setup(**kwargs) -> mrsm.SuccessTuple:\n"
|
68
|
+
" \"\"\"Executed during installation and `mrsm setup plugin {plugin_name}`.\"\"\"\n"
|
69
|
+
" return True, \"Success\"\n\n\n"
|
70
|
+
),
|
71
|
+
'register': (
|
72
|
+
"def register(pipe: mrsm.Pipe):\n"
|
73
|
+
" \"\"\"Return the default parameters for a new pipe.\"\"\"\n"
|
74
|
+
" return {{\n"
|
75
|
+
" 'columns': {{\n"
|
76
|
+
" 'datetime': {dt_col_name},\n"
|
77
|
+
" }}\n"
|
78
|
+
" }}\n\n\n"
|
79
|
+
),
|
80
|
+
'fetch': (
|
81
|
+
"def fetch(\n"
|
82
|
+
" pipe: mrsm.Pipe,\n"
|
83
|
+
" begin: datetime | None = None,\n"
|
84
|
+
" end: datetime | None = None,\n"
|
85
|
+
" **kwargs\n"
|
86
|
+
"):\n"
|
87
|
+
" \"\"\"Return or yield dataframes.\"\"\"\n"
|
88
|
+
" docs = []\n"
|
89
|
+
" # populate docs with dictionaries (rows).\n"
|
90
|
+
" return docs\n\n\n"
|
91
|
+
),
|
92
|
+
'connector': (
|
93
|
+
"@make_connector\n"
|
94
|
+
"class {plugin_name_capitalized}Connector(Connector):\n"
|
95
|
+
" \"\"\"Implement '{plugin_name_lower}' connectors.\"\"\"\n\n"
|
96
|
+
" REQUIRED_ATTRIBUTES: list[str] = []\n"
|
97
|
+
" \n"
|
98
|
+
" def fetch(\n"
|
99
|
+
" self,\n"
|
100
|
+
" pipe: mrsm.Pipe,\n"
|
101
|
+
" begin: datetime | None = None,\n"
|
102
|
+
" end: datetime | None = None,\n"
|
103
|
+
" **kwargs\n"
|
104
|
+
" ):\n"
|
105
|
+
" \"\"\"Return or yield dataframes.\"\"\"\n"
|
106
|
+
" docs = []\n"
|
107
|
+
" # populate docs with dictionaries (rows).\n"
|
108
|
+
" return docs\n\n\n"
|
109
|
+
),
|
110
|
+
'action': (
|
111
|
+
"@make_action\n"
|
112
|
+
"def {action_name}(**kwargs) -> mrsm.SuccessTuple:\n"
|
113
|
+
" \"\"\"Run `mrsm {action_spaces}` to trigger.\"\"\"\n"
|
114
|
+
" return True, \"Success\"\n\n\n"
|
115
|
+
),
|
116
|
+
'api': (
|
117
|
+
"@api_plugin\n"
|
118
|
+
"def init_app(fastapi_app):\n"
|
119
|
+
" \"\"\"Add new endpoints to the FastAPI app.\"\"\"\n\n"
|
120
|
+
" import fastapi\n"
|
121
|
+
" from meerschaum.api import manager\n\n"
|
122
|
+
" @fastapi_app.get('/{plugin_name}')\n"
|
123
|
+
" def get_my_endpoint(curr_user=fastapi.Depends(manager)):\n"
|
124
|
+
" return {{'message': \"Hello from plugin '{plugin_name}'!\"}}\n\n\n"
|
125
|
+
),
|
126
|
+
'web': (
|
127
|
+
"@dash_plugin\n"
|
128
|
+
"def init_dash(dash_app):\n"
|
129
|
+
" \"\"\"Initialize the Plotly Dash application.\"\"\"\n\n"
|
130
|
+
" import dash.html as html\n"
|
131
|
+
" import dash.dcc as dcc\n"
|
132
|
+
" from dash import Input, Output, State, no_update\n"
|
133
|
+
" import dash_bootstrap_components as dbc\n\n"
|
134
|
+
" # Create a new page at the path `/dash/{plugin_name}`.\n"
|
135
|
+
" @web_page('{plugin_name}', login_required=False)\n"
|
136
|
+
" def page_layout():\n"
|
137
|
+
" \"\"\"Return the layout objects for this page.\"\"\"\n"
|
138
|
+
" return dbc.Container([\n"
|
139
|
+
" dcc.Location(id='{plugin_name}-location'),\n"
|
140
|
+
" html.Div(id='output-div'),\n"
|
141
|
+
" ])\n\n"
|
142
|
+
" @dash_app.callback(\n"
|
143
|
+
" Output('output-div', 'children'),\n"
|
144
|
+
" Input('{plugin_name}-location', 'pathname'),\n"
|
145
|
+
" )\n"
|
146
|
+
" def render_page_on_url_change(pathname: str):\n"
|
147
|
+
" \"\"\"Reload page contents when the URL path changes.\"\"\"\n"
|
148
|
+
" return html.H1(\"Hello from plugin '{plugin_name}'!\")\n\n\n"
|
149
|
+
),
|
150
|
+
}
|
151
|
+
|
152
|
+
def bootstrap_plugin(
|
153
|
+
plugin_name: str,
|
154
|
+
debug: bool = False,
|
155
|
+
**kwargs: Any
|
156
|
+
) -> SuccessTuple:
|
157
|
+
"""
|
158
|
+
Prompt the user for features and create a plugin file.
|
159
|
+
"""
|
160
|
+
from meerschaum.utils.misc import edit_file
|
161
|
+
plugins_dir_path = _get_plugins_dir_path()
|
162
|
+
clear_screen(debug=debug)
|
163
|
+
info(
|
164
|
+
"Answer the questions below to pick out features.\n"
|
165
|
+
+ " See the Writing Plugins guide for documentation:\n"
|
166
|
+
+ " https://meerschaum.io/reference/plugins/writing-plugins/ "
|
167
|
+
+ "for documentation.\n"
|
168
|
+
)
|
169
|
+
|
170
|
+
plugin = mrsm.Plugin(plugin_name)
|
171
|
+
if plugin.is_installed():
|
172
|
+
uninstall_success, uninstall_msg = _ask_to_uninstall(plugin)
|
173
|
+
if not uninstall_success:
|
174
|
+
return uninstall_success, uninstall_msg
|
175
|
+
clear_screen(debug=debug)
|
176
|
+
|
177
|
+
features: List[str] = choose(
|
178
|
+
"Which of the following features would you like to add to your plugin?",
|
179
|
+
list(FEATURE_CHOICES.items()),
|
180
|
+
default = 'fetch',
|
181
|
+
multiple = True,
|
182
|
+
as_indices = True,
|
183
|
+
**kwargs
|
184
|
+
)
|
185
|
+
|
186
|
+
clear_screen(debug=debug)
|
187
|
+
|
188
|
+
action_name = ''
|
189
|
+
if 'action' in features:
|
190
|
+
action_name = _get_action_name()
|
191
|
+
clear_screen(debug=debug)
|
192
|
+
action_spaces = action_name.replace('_', ' ')
|
193
|
+
|
194
|
+
dt_col_name = None
|
195
|
+
if 'fetch' in features:
|
196
|
+
dt_col_name = _get_quoted_dt_col_name()
|
197
|
+
clear_screen(debug=debug)
|
198
|
+
|
199
|
+
plugin_labels = {
|
200
|
+
'plugin_name': plugin_name,
|
201
|
+
'plugin_name_capitalized': re.split(
|
202
|
+
r'[-_]', plugin_name.lower()
|
203
|
+
)[0].capitalize(),
|
204
|
+
'plugin_name_lower': plugin_name.lower(),
|
205
|
+
'action_name': action_name,
|
206
|
+
'action_spaces': action_spaces,
|
207
|
+
'dt_col_name': dt_col_name,
|
208
|
+
}
|
209
|
+
|
210
|
+
body_text = ""
|
211
|
+
body_text += FEATURE_LINES['header'].format(**plugin_labels)
|
212
|
+
body_text += IMPORTS_LINES['stdlib'].format(**plugin_labels)
|
213
|
+
body_text += IMPORTS_LINES['default'].format(**plugin_labels)
|
214
|
+
if 'connector' in features:
|
215
|
+
body_text += IMPORTS_LINES['connector'].format(**plugin_labels)
|
216
|
+
if 'action' in features:
|
217
|
+
body_text += IMPORTS_LINES['action'].format(**plugin_labels)
|
218
|
+
if 'api' in features and 'web' in features:
|
219
|
+
body_text += IMPORTS_LINES['api+web'].format(**plugin_labels)
|
220
|
+
elif 'api' in features:
|
221
|
+
body_text += IMPORTS_LINES['api'].format(**plugin_labels)
|
222
|
+
elif 'web' in features:
|
223
|
+
body_text += IMPORTS_LINES['web'].format(**plugin_labels)
|
224
|
+
|
225
|
+
body_text += "\n"
|
226
|
+
body_text += FEATURE_LINES['default'].format(**plugin_labels)
|
227
|
+
body_text += FEATURE_LINES['setup'].format(**plugin_labels)
|
228
|
+
|
229
|
+
if 'fetch' in features:
|
230
|
+
body_text += FEATURE_LINES['register'].format(**plugin_labels)
|
231
|
+
body_text += FEATURE_LINES['fetch'].format(**plugin_labels)
|
232
|
+
|
233
|
+
if 'connector' in features:
|
234
|
+
body_text += FEATURE_LINES['connector'].format(**plugin_labels)
|
235
|
+
|
236
|
+
if 'action' in features:
|
237
|
+
body_text += FEATURE_LINES['action'].format(**plugin_labels)
|
238
|
+
|
239
|
+
if 'api' in features:
|
240
|
+
body_text += FEATURE_LINES['api'].format(**plugin_labels)
|
241
|
+
|
242
|
+
if 'web' in features:
|
243
|
+
body_text += FEATURE_LINES['web'].format(**plugin_labels)
|
244
|
+
|
245
|
+
try:
|
246
|
+
plugin_path = plugins_dir_path / (plugin_name + '.py')
|
247
|
+
with open(plugin_path, 'w+', encoding='utf-8') as f:
|
248
|
+
f.write(body_text.rstrip())
|
249
|
+
except Exception as e:
|
250
|
+
error_msg = f"Failed to write file '{plugin_path}':\n{e}"
|
251
|
+
return False, error_msg
|
252
|
+
|
253
|
+
clear_screen(debug=debug)
|
254
|
+
mrsm.pprint((True, f"Successfully created file '{plugin_path}'."))
|
255
|
+
try:
|
256
|
+
_ = prompt(
|
257
|
+
f"Press [Enter] to edit plugin '{plugin_name}',"
|
258
|
+
+ " [CTRL+C] to skip.",
|
259
|
+
icon = False,
|
260
|
+
)
|
261
|
+
except (KeyboardInterrupt, Exception):
|
262
|
+
return True, "Success"
|
263
|
+
|
264
|
+
edit_file(plugin_path, debug=debug)
|
265
|
+
return True, "Success"
|
266
|
+
|
267
|
+
|
268
|
+
def _get_plugins_dir_path() -> pathlib.Path:
|
269
|
+
from meerschaum.config.paths import PLUGINS_DIR_PATHS
|
270
|
+
|
271
|
+
if not PLUGINS_DIR_PATHS:
|
272
|
+
raise EnvironmentError("No plugin dir path could be found.")
|
273
|
+
|
274
|
+
if len(PLUGINS_DIR_PATHS) == 1:
|
275
|
+
return PLUGINS_DIR_PATHS[0]
|
276
|
+
|
277
|
+
return pathlib.Path(
|
278
|
+
choose(
|
279
|
+
"In which directory do you want to write your plugin?",
|
280
|
+
[path.as_posix() for path in PLUGINS_DIR_PATHS],
|
281
|
+
numeric = True,
|
282
|
+
multiple = False,
|
283
|
+
default = PLUGINS_DIR_PATHS[0].as_posix(),
|
284
|
+
)
|
285
|
+
)
|
286
|
+
|
287
|
+
|
288
|
+
def _ask_to_uninstall(plugin: mrsm.Plugin, **kwargs: Any) -> SuccessTuple:
|
289
|
+
from meerschaum._internal.entry import entry
|
290
|
+
warn(f"Plugin '{plugin}' is already installed!", stack=False)
|
291
|
+
uninstall_plugin = yes_no(
|
292
|
+
f"Do you want to first uninstall '{plugin}'?",
|
293
|
+
default = 'n',
|
294
|
+
**kwargs
|
295
|
+
)
|
296
|
+
if not uninstall_plugin:
|
297
|
+
return False, f"Plugin '{plugin}' already exists."
|
298
|
+
|
299
|
+
return entry(['uninstall', 'plugin', plugin.name, '-f'])
|
300
|
+
|
301
|
+
|
302
|
+
def _get_action_name() -> str:
|
303
|
+
while True:
|
304
|
+
try:
|
305
|
+
action_name = prompt(
|
306
|
+
"What is name of your action?\n "
|
307
|
+
+ "(separate subactions with spaces, e.g. `sing song`):"
|
308
|
+
).replace(' ', '_')
|
309
|
+
except KeyboardInterrupt:
|
310
|
+
return False, "Aborted plugin creation."
|
311
|
+
|
312
|
+
if action_name:
|
313
|
+
break
|
314
|
+
warn("Please enter an action.", stack=False)
|
315
|
+
return action_name
|
316
|
+
|
317
|
+
|
318
|
+
def _get_quoted_dt_col_name() -> str:
|
319
|
+
try:
|
320
|
+
dt_col_name = prompt(
|
321
|
+
"Enter the datetime column name ([CTRL+C] to skip):"
|
322
|
+
)
|
323
|
+
except (Exception, KeyboardInterrupt):
|
324
|
+
dt_col_name = None
|
325
|
+
|
326
|
+
if dt_col_name is None:
|
327
|
+
dt_col_name = 'None'
|
328
|
+
elif '"' in dt_col_name or "'" in dt_col_name:
|
329
|
+
dt_col_name = f"\"\"\"{dt_col_name}\"\"\""
|
330
|
+
else:
|
331
|
+
dt_col_name = f"\"{dt_col_name}\""
|
332
|
+
|
333
|
+
return dt_col_name
|
@@ -185,7 +185,7 @@ class Daemon:
|
|
185
185
|
result = self.target(*self.target_args, **self.target_kw)
|
186
186
|
self.properties['result'] = result
|
187
187
|
except Exception as e:
|
188
|
-
warn(e, stacklevel=3)
|
188
|
+
warn(f"Exception in daemon target function: {e}", stacklevel=3)
|
189
189
|
result = e
|
190
190
|
finally:
|
191
191
|
self._log_refresh_timer.cancel()
|
@@ -203,9 +203,20 @@ class Daemon:
|
|
203
203
|
daemon_error = traceback.format_exc()
|
204
204
|
with open(DAEMON_ERROR_LOG_PATH, 'a+', encoding='utf-8') as f:
|
205
205
|
f.write(daemon_error)
|
206
|
+
warn(f"Encountered an error while running the daemon '{self}':\n{daemon_error}")
|
207
|
+
finally:
|
208
|
+
self._cleanup_on_exit()
|
206
209
|
|
207
|
-
|
208
|
-
|
210
|
+
def _cleanup_on_exit(self):
|
211
|
+
"""Perform cleanup operations when the daemon exits."""
|
212
|
+
try:
|
213
|
+
self._log_refresh_timer.cancel()
|
214
|
+
self.rotating_log.close()
|
215
|
+
if self.pid is None and self.pid_path.exists():
|
216
|
+
self.pid_path.unlink()
|
217
|
+
self._capture_process_timestamp('ended')
|
218
|
+
except Exception as e:
|
219
|
+
warn(f"Error during daemon cleanup: {e}")
|
209
220
|
|
210
221
|
|
211
222
|
def _capture_process_timestamp(
|
@@ -65,24 +65,31 @@ class FileDescriptorInterceptor:
|
|
65
65
|
except BlockingIOError:
|
66
66
|
continue
|
67
67
|
except OSError as e:
|
68
|
-
|
68
|
+
from meerschaum.utils.warnings import warn
|
69
|
+
warn(f"OSError in FileDescriptorInterceptor: {e}")
|
70
|
+
break
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
+
try:
|
73
|
+
first_char_is_newline = data[0] == b'\n'
|
74
|
+
last_char_is_newline = data[-1] == b'\n'
|
72
75
|
|
73
|
-
|
74
|
-
|
76
|
+
injected_str = self.injection_hook()
|
77
|
+
injected_bytes = injected_str.encode('utf-8')
|
75
78
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
+
if is_first_read:
|
80
|
+
data = b'\n' + data
|
81
|
+
is_first_read = False
|
79
82
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
83
|
+
modified_data = (
|
84
|
+
(data[:-1].replace(b'\n', b'\n' + injected_bytes) + b'\n')
|
85
|
+
if last_char_is_newline
|
86
|
+
else data.replace(b'\n', b'\n' + injected_bytes)
|
87
|
+
)
|
88
|
+
os.write(self.new_file_descriptor, modified_data)
|
89
|
+
except Exception as e:
|
90
|
+
from meerschaum.utils.warnings import warn
|
91
|
+
warn(f"Error in FileDescriptorInterceptor data processing: {e}")
|
92
|
+
break
|
86
93
|
|
87
94
|
|
88
95
|
def stop_interception(self):
|
@@ -355,26 +355,29 @@ class RotatingFile(io.IOBase):
|
|
355
355
|
As such, if data is larger than max_file_size, then the corresponding subfile
|
356
356
|
may exceed this limit.
|
357
357
|
"""
|
358
|
-
self.file_path.parent.mkdir(exist_ok=True, parents=True)
|
359
|
-
if isinstance(data, bytes):
|
360
|
-
data = data.decode('utf-8')
|
361
|
-
|
362
|
-
prefix_str = self.get_timestamp_prefix_str() if self.write_timestamps else ""
|
363
|
-
suffix_str = "\n" if self.write_timestamps else ""
|
364
|
-
self.refresh_files(
|
365
|
-
potential_new_len = len(prefix_str + data + suffix_str),
|
366
|
-
start_interception = self.write_timestamps,
|
367
|
-
)
|
368
358
|
try:
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
359
|
+
self.file_path.parent.mkdir(exist_ok=True, parents=True)
|
360
|
+
if isinstance(data, bytes):
|
361
|
+
data = data.decode('utf-8')
|
362
|
+
|
363
|
+
prefix_str = self.get_timestamp_prefix_str() if self.write_timestamps else ""
|
364
|
+
suffix_str = "\n" if self.write_timestamps else ""
|
365
|
+
self.refresh_files(
|
366
|
+
potential_new_len = len(prefix_str + data + suffix_str),
|
367
|
+
start_interception = self.write_timestamps,
|
368
|
+
)
|
369
|
+
try:
|
370
|
+
if prefix_str:
|
371
|
+
self._current_file_obj.write(prefix_str)
|
372
|
+
self._current_file_obj.write(data)
|
373
|
+
if suffix_str:
|
374
|
+
self._current_file_obj.write(suffix_str)
|
375
|
+
except Exception as e:
|
376
|
+
warn(f"Failed to write to subfile:\n{traceback.format_exc()}")
|
377
|
+
self.flush()
|
378
|
+
self.delete(unused_only=True)
|
374
379
|
except Exception as e:
|
375
|
-
warn(f"
|
376
|
-
self.flush()
|
377
|
-
self.delete(unused_only=True)
|
380
|
+
warn(f"Unexpected error in RotatingFile.write: {e}")
|
378
381
|
|
379
382
|
|
380
383
|
def delete(self, unused_only: bool = False) -> None:
|
@@ -10,7 +10,7 @@ from __future__ import annotations
|
|
10
10
|
import platform
|
11
11
|
import os
|
12
12
|
import sys
|
13
|
-
from meerschaum.utils.typing import Optional, Union, Any
|
13
|
+
from meerschaum.utils.typing import Optional, Union, Any, Dict
|
14
14
|
from meerschaum.utils.formatting._shell import make_header
|
15
15
|
from meerschaum.utils.formatting._pprint import pprint
|
16
16
|
from meerschaum.utils.formatting._pipes import (
|
@@ -298,6 +298,7 @@ def print_options(
|
|
298
298
|
header: Optional[str] = None,
|
299
299
|
num_cols: Optional[int] = None,
|
300
300
|
adjust_cols: bool = True,
|
301
|
+
sort_options: bool = False,
|
301
302
|
**kw
|
302
303
|
) -> None:
|
303
304
|
"""
|
@@ -339,6 +340,8 @@ def print_options(
|
|
339
340
|
_options = []
|
340
341
|
for o in options:
|
341
342
|
_options.append(str(o))
|
343
|
+
if sort_options:
|
344
|
+
_options = sorted(_options)
|
342
345
|
_header = f"Available {name}" if header is None else header
|
343
346
|
|
344
347
|
if num_cols is None:
|
@@ -349,7 +352,7 @@ def print_options(
|
|
349
352
|
print()
|
350
353
|
print(make_header(_header))
|
351
354
|
### print actions
|
352
|
-
for option in
|
355
|
+
for option in _options:
|
353
356
|
if not nopretty:
|
354
357
|
print(" - ", end="")
|
355
358
|
print(option)
|
@@ -385,7 +388,7 @@ def print_options(
|
|
385
388
|
|
386
389
|
if _header is not None:
|
387
390
|
table = Table(
|
388
|
-
title = '\n' + _header,
|
391
|
+
title = ('\n' + _header) if header else header,
|
389
392
|
box = box.SIMPLE,
|
390
393
|
show_header = False,
|
391
394
|
show_footer = False,
|
@@ -397,13 +400,22 @@ def print_options(
|
|
397
400
|
for i in range(num_cols):
|
398
401
|
table.add_column()
|
399
402
|
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
403
|
+
if len(_options) < 12:
|
404
|
+
# If fewer than 12 items, use a single column
|
405
|
+
for option in _options:
|
406
|
+
table.add_row(Text.from_ansi(highlight_pipes(option)))
|
407
|
+
else:
|
408
|
+
# Otherwise, use multiple columns as before
|
409
|
+
num_rows = (len(_options) + num_cols - 1) // num_cols
|
410
|
+
for i in range(num_rows):
|
411
|
+
row = []
|
412
|
+
for j in range(num_cols):
|
413
|
+
index = i + j * num_rows
|
414
|
+
if index < len(_options):
|
415
|
+
row.append(Text.from_ansi(highlight_pipes(_options[index])))
|
416
|
+
else:
|
417
|
+
row.append('')
|
418
|
+
table.add_row(*row)
|
407
419
|
|
408
420
|
get_console().print(table)
|
409
421
|
return None
|
@@ -42,6 +42,7 @@ packages: Dict[str, Dict[str, str]] = {
|
|
42
42
|
'requests' : 'requests>=2.23.0',
|
43
43
|
'binaryornot' : 'binaryornot>=0.4.4',
|
44
44
|
'pyvim' : 'pyvim>=3.0.2',
|
45
|
+
'ptpython' : 'ptpython>=3.0.27',
|
45
46
|
'aiofiles' : 'aiofiles>=0.6.0',
|
46
47
|
'packaging' : 'packaging>=21.3.0',
|
47
48
|
'prompt_toolkit' : 'prompt-toolkit>=3.0.39',
|
@@ -74,7 +75,6 @@ packages: Dict[str, Dict[str, str]] = {
|
|
74
75
|
'litecli' : 'litecli>=1.5.0',
|
75
76
|
'mssqlcli' : 'mssql-cli>=1.0.0',
|
76
77
|
'gadwall' : 'gadwall>=0.2.0',
|
77
|
-
'ptpython' : 'ptpython>=3.0.27',
|
78
78
|
},
|
79
79
|
'stack': {
|
80
80
|
'compose' : 'docker-compose>=1.29.2',
|