meerschaum 2.2.0rc1__py3-none-any.whl → 2.2.0rc2__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 (34) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/actions/show.py +68 -43
  3. meerschaum/api/dash/callbacks/dashboard.py +2 -7
  4. meerschaum/api/dash/pipes.py +33 -9
  5. meerschaum/api/dash/plugins.py +25 -9
  6. meerschaum/api/resources/templates/termpage.html +3 -0
  7. meerschaum/api/routes/_login.py +5 -4
  8. meerschaum/api/routes/_plugins.py +6 -3
  9. meerschaum/config/_dash.py +11 -0
  10. meerschaum/config/_default.py +3 -1
  11. meerschaum/config/_jobs.py +10 -4
  12. meerschaum/config/_paths.py +1 -0
  13. meerschaum/config/_sync.py +2 -3
  14. meerschaum/config/_version.py +1 -1
  15. meerschaum/config/stack/__init__.py +6 -6
  16. meerschaum/config/stack/grafana/__init__.py +1 -1
  17. meerschaum/config/static/__init__.py +3 -1
  18. meerschaum/connectors/sql/_plugins.py +0 -2
  19. meerschaum/core/User/_User.py +156 -16
  20. meerschaum/core/User/__init__.py +1 -1
  21. meerschaum/plugins/_Plugin.py +1 -1
  22. meerschaum/utils/daemon/Daemon.py +23 -12
  23. meerschaum/utils/daemon/FileDescriptorInterceptor.py +46 -4
  24. meerschaum/utils/daemon/RotatingFile.py +76 -57
  25. meerschaum/utils/daemon/__init__.py +1 -0
  26. meerschaum/utils/threading.py +1 -0
  27. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/METADATA +1 -1
  28. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/RECORD +34 -33
  29. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/LICENSE +0 -0
  30. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/NOTICE +0 -0
  31. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/WHEEL +0 -0
  32. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/entry_points.txt +0 -0
  33. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/top_level.txt +0 -0
  34. {meerschaum-2.2.0rc1.dist-info → meerschaum-2.2.0rc2.dist-info}/zip-safe +0 -0
meerschaum/__main__.py CHANGED
@@ -4,7 +4,7 @@
4
4
  # vim:fenc=utf-8
5
5
 
6
6
  """
7
- Copyright 2021 Bennett Meares
7
+ Copyright 2024 Bennett Meares
8
8
 
9
9
  Licensed under the Apache License, Version 2.0 (the "License");
10
10
  you may not use this file except in compliance with the License.
@@ -578,7 +578,7 @@ def _show_logs(
578
578
  `show logs myjob myotherjob`
579
579
  """
580
580
  import os, pathlib, random, asyncio
581
- from datetime import datetime
581
+ from datetime import datetime, timezone
582
582
  from meerschaum.utils.packages import attempt_import, import_rich
583
583
  from meerschaum.utils.daemon import get_filtered_daemons, Daemon
584
584
  from meerschaum.utils.warnings import warn, info
@@ -589,81 +589,106 @@ def _show_logs(
589
589
  if not ANSI:
590
590
  info = print
591
591
  colors = get_config('jobs', 'logs', 'colors')
592
+ timestamp_format = get_config('jobs', 'logs', 'timestamp_format')
593
+ follow_timestamp_format = get_config('jobs', 'logs', 'follow_timestamp_format')
592
594
  daemons = get_filtered_daemons(action)
593
-
594
- def _build_buffer_spaces(daemons) -> Dict[str, str]:
595
- _max_len_id = max([len(d.daemon_id) for d in daemons]) if daemons else 0
596
- _buffer_len = max(get_config('jobs', 'logs', 'min_buffer_len'), _max_len_id + 2)
595
+ now = datetime.now(timezone.utc)
596
+ now_str = now.strftime(timestamp_format)
597
+ now_follow_str = now.strftime(follow_timestamp_format)
598
+
599
+ def build_buffer_spaces(daemons) -> Dict[str, str]:
600
+ max_len_id = max(len(d.daemon_id) for d in daemons) if daemons else 0
601
+ buffer_len = max(
602
+ get_config('jobs', 'logs', 'min_buffer_len'),
603
+ max_len_id
604
+ )
597
605
  return {
598
- d.daemon_id: ''.join([' ' for i in range(_buffer_len - len(d.daemon_id))])
606
+ d.daemon_id: ''.join([' ' for i in range(buffer_len - len(d.daemon_id))])
599
607
  for d in daemons
600
608
  }
601
609
 
602
- def _build_job_colors(daemons, _old_job_colors = None) -> Dict[str, str]:
610
+ def build_job_colors(daemons, _old_job_colors = None) -> Dict[str, str]:
603
611
  return {d.daemon_id: colors[i % len(colors)] for i, d in enumerate(daemons)}
604
612
 
605
- _buffer_spaces = _build_buffer_spaces(daemons)
606
- _job_colors = _build_job_colors(daemons)
613
+ buffer_spaces = build_buffer_spaces(daemons)
614
+ job_colors = build_job_colors(daemons)
607
615
 
608
- def _get_buffer_spaces(daemon_id):
609
- nonlocal _buffer_spaces, daemons
610
- if daemon_id not in _buffer_spaces:
616
+ def get_buffer_spaces(daemon_id):
617
+ nonlocal buffer_spaces, daemons
618
+ if daemon_id not in buffer_spaces:
611
619
  d = Daemon(daemon_id=daemon_id)
612
620
  if d not in daemons:
613
621
  daemons = get_filtered_daemons(action)
614
- _buffer_spaces = _build_buffer_spaces(daemons)
615
- return _buffer_spaces[daemon_id]
622
+ buffer_spaces = build_buffer_spaces(daemons)
623
+ return buffer_spaces[daemon_id] or ' '
616
624
 
617
- def _get_job_colors(daemon_id):
618
- nonlocal _job_colors, daemons
619
- if daemon_id not in _job_colors:
625
+ def get_job_colors(daemon_id):
626
+ nonlocal job_colors, daemons
627
+ if daemon_id not in job_colors:
620
628
  d = Daemon(daemon_id=daemon_id)
621
629
  if d not in daemons:
622
630
  daemons = get_filtered_daemons(action)
623
- _job_colors = _build_job_colors(daemons)
624
- return _job_colors[daemon_id]
631
+ job_colors = build_job_colors(daemons)
632
+ return job_colors[daemon_id]
625
633
 
626
- def _follow_pretty_print():
634
+ def follow_pretty_print():
627
635
  watchfiles = attempt_import('watchfiles')
628
636
  rich = import_rich()
629
637
  rich_text = attempt_import('rich.text')
630
- _watch_daemon_ids = {d.daemon_id: d for d in daemons}
638
+ watch_daemon_ids = {d.daemon_id: d for d in daemons}
631
639
  info("Watching log files...")
632
640
 
633
- def _print_job_line(daemon, line):
634
- date_prefix_str = line[:len('YYYY-MM-DD HH:mm')]
641
+ previous_line_timestamp = None
642
+ def print_job_line(daemon, line):
643
+ nonlocal previous_line_timestamp
644
+ date_prefix_str = line[:len(now_str)]
635
645
  try:
636
- line_timestamp = datetime.fromisoformat(date_prefix_str)
646
+ line_timestamp = datetime.strptime(date_prefix_str, timestamp_format)
647
+ previous_line_timestamp = line_timestamp
637
648
  except Exception as e:
638
649
  line_timestamp = None
639
650
  if line_timestamp:
640
- line = line[len('YYYY-MM-DD HH:mm | '):]
641
- if len(line) == 0:
651
+ line = line[(len(now_str) + 3):]
652
+ else:
653
+ line_timestamp = previous_line_timestamp
654
+
655
+ if len(line) == 0 or line == '\n':
642
656
  return
657
+
643
658
  text = rich_text.Text(daemon.daemon_id)
644
- text.append(
645
- _get_buffer_spaces(daemon.daemon_id) + '| '
646
- + (line[:-1] if line[-1] == '\n' else line)
659
+ line_prefix = (
660
+ get_buffer_spaces(daemon.daemon_id)
661
+ + (line_timestamp.strftime(follow_timestamp_format) if line_timestamp else '')
662
+ + ' | '
647
663
  )
664
+ text.append(line_prefix + (line[:-1] if line[-1] == '\n' else line))
648
665
  if ANSI:
649
666
  text.stylize(
650
- _get_job_colors(daemon.daemon_id),
667
+ get_job_colors(daemon.daemon_id),
651
668
  0,
652
- len(daemon.daemon_id) + len(_get_buffer_spaces(daemon.daemon_id)) + 1
669
+ len(daemon.daemon_id) + len(line_prefix)
653
670
  )
654
671
  get_console().print(text)
655
672
 
656
673
 
657
- def _print_log_lines(daemon):
674
+ def print_log_lines(daemon):
658
675
  for line in daemon.readlines():
659
- _print_job_line(daemon, line)
676
+ print_job_line(daemon, line)
660
677
 
661
- def _seek_back_offset(d) -> bool:
678
+ def seek_back_offset(d) -> bool:
662
679
  if d.log_offset_path.exists():
663
680
  d.log_offset_path.unlink()
664
681
 
665
682
  latest_subfile_path = d.rotating_log.get_latest_subfile_path()
666
683
  latest_subfile_index = d.rotating_log.get_latest_subfile_index()
684
+
685
+ ### Sometimes the latest file is empty.
686
+ if os.stat(latest_subfile_path).st_size == 0 and latest_subfile_index > 0:
687
+ latest_subfile_index -= 1
688
+ latest_subfile_path = d.rotating_log.get_subfile_path_from_index(
689
+ latest_subfile_index
690
+ )
691
+
667
692
  with open(latest_subfile_path, 'r', encoding='utf-8') as f:
668
693
  latest_lines = f.readlines()
669
694
 
@@ -683,11 +708,11 @@ def _show_logs(
683
708
  for d in daemons
684
709
  }
685
710
  for d in daemons:
686
- _seek_back_offset(d)
687
- _print_log_lines(d)
711
+ seek_back_offset(d)
712
+ print_log_lines(d)
688
713
 
689
714
  _quit = False
690
- def _watch_logs():
715
+ def watch_logs():
691
716
  for changes in watchfiles.watch(LOGS_RESOURCES_PATH):
692
717
  if _quit:
693
718
  return
@@ -699,7 +724,7 @@ def _show_logs(
699
724
  if not file_path.exists():
700
725
  continue
701
726
  daemon_id = file_path.name.split('.log')[0]
702
- if daemon_id not in _watch_daemon_ids and action:
727
+ if daemon_id not in watch_daemon_ids and action:
703
728
  continue
704
729
  try:
705
730
  daemon = Daemon(daemon_id=daemon_id)
@@ -708,21 +733,21 @@ def _show_logs(
708
733
  warn(f"Seeing new logs for non-existent job '{daemon_id}'.", stack=False)
709
734
 
710
735
  if daemon is not None:
711
- _print_log_lines(daemon)
736
+ print_log_lines(daemon)
712
737
 
713
738
  try:
714
- _watch_logs()
739
+ watch_logs()
715
740
  except KeyboardInterrupt as ki:
716
741
  _quit = True
717
742
 
718
- def _print_nopretty_log_text():
743
+ def print_nopretty_log_text():
719
744
  for d in daemons:
720
745
  log_text = d.log_text
721
746
  print(d.daemon_id)
722
747
  print(log_text)
723
748
 
724
- _print_log_text = _follow_pretty_print if not nopretty else _print_nopretty_log_text
725
- _print_log_text()
749
+ print_log_text = follow_pretty_print if not nopretty else print_nopretty_log_text
750
+ print_log_text()
726
751
 
727
752
  return True, "Success"
728
753
 
@@ -440,18 +440,13 @@ def update_flags(input_flags_dropdown_values, n_clicks, input_flags_texts):
440
440
  className = 'input-text',
441
441
  )
442
442
 
443
+ remove_index = trigger_dict['index'] if trigger_type == 'input-flags-remove-button' else None
443
444
  rows = [
444
445
  build_row(i, val, val_text)
445
446
  for i, (val, val_text) in enumerate(zip(input_flags_dropdown_values, input_flags_texts))
447
+ if i != remove_index
446
448
  ]
447
449
 
448
- if trigger_type == 'input-flags-remove-button':
449
- remove_index = trigger_dict['index']
450
- try:
451
- del rows[remove_index]
452
- except IndexError:
453
- pass
454
-
455
450
  if not rows or input_flags_dropdown_values[-1]:
456
451
  rows.append(build_row(len(rows), None, None))
457
452
 
@@ -24,6 +24,7 @@ from meerschaum.api.dash import (
24
24
  from meerschaum.api.dash.connectors import get_web_connector
25
25
  from meerschaum.api.dash.components import alert_from_success_tuple
26
26
  from meerschaum.api.dash.users import is_session_authenticated
27
+ from meerschaum.config import get_config
27
28
  import meerschaum as mrsm
28
29
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
29
30
  dash_ace = attempt_import('dash_ace', lazy=False, check_update=CHECK_UPDATE)
@@ -110,12 +111,16 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
110
111
  session_id = (session_data or {}).get('session-id', None)
111
112
  authenticated = is_session_authenticated(str(session_id))
112
113
 
113
- _pipes = pipes_from_state(*keys, as_list=True)
114
- alerts = [alert_from_success_tuple(_pipes)]
115
- if not isinstance(_pipes, list):
116
- _pipes = []
117
- for p in _pipes:
118
- meta_str = json.dumps(p.meta)
114
+ pipes = pipes_from_state(*keys, as_list=True)
115
+ alerts = [alert_from_success_tuple(pipes)]
116
+ if not isinstance(pipes, list):
117
+ pipes = []
118
+
119
+ max_num_pipes_cards = get_config('dash', 'max_num_pipes_cards')
120
+ overflow_pipes = pipes[max_num_pipes_cards:]
121
+
122
+ for pipe in pipes[:max_num_pipes_cards]:
123
+ meta_str = json.dumps(pipe.meta)
119
124
  footer_children = dbc.Row(
120
125
  [
121
126
  dbc.Col(
@@ -188,13 +193,13 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
188
193
  )
189
194
  card_body_children = [
190
195
  html.H5(
191
- html.B(str(p)),
196
+ html.B(str(pipe)),
192
197
  className = 'card-title',
193
198
  style = {'font-family': ['monospace']}
194
199
  ),
195
200
  html.Div(
196
201
  dbc.Accordion(
197
- accordion_items_from_pipe(p, authenticated=authenticated),
202
+ accordion_items_from_pipe(pipe, authenticated=authenticated),
198
203
  flush = True,
199
204
  start_collapsed = True,
200
205
  id = {'type': 'pipe-accordion', 'index': meta_str},
@@ -203,11 +208,30 @@ def get_pipes_cards(*keys, session_data: Optional[Dict[str, Any]] = None):
203
208
 
204
209
  ]
205
210
  cards.append(
206
- dbc.Card(children=[
211
+ dbc.Card([
207
212
  dbc.CardBody(children=card_body_children),
208
213
  dbc.CardFooter(children=footer_children),
209
214
  ])
210
215
  )
216
+
217
+ if overflow_pipes:
218
+ cards.append(
219
+ dbc.Card([
220
+ dbc.CardBody(
221
+ html.Ul(
222
+ [
223
+ html.Li(html.H5(
224
+ html.B(str(pipe)),
225
+ className = 'card-title',
226
+ style = {'font-family': ['monospace']}
227
+ ))
228
+ for pipe in overflow_pipes
229
+ ]
230
+ )
231
+ )
232
+ ])
233
+ )
234
+
211
235
  return cards, alerts
212
236
 
213
237
 
@@ -32,17 +32,22 @@ def get_plugins_cards(
32
32
  desc = get_api_connector().get_plugin_attributes(plugin).get(
33
33
  'description', 'No description provided.'
34
34
  )
35
- desc_textarea_kw = dict(
36
- value=desc, readOnly=True, debounce=False, className='plugin-description',
37
- draggable=False, wrap='overflow',
38
- id={'type': 'description-textarea', 'index': plugin_name},
39
- )
35
+ desc_textarea_kw = {
36
+ 'value': desc,
37
+ 'readOnly': True,
38
+ 'debounce': False,
39
+ 'className': 'plugin-description',
40
+ 'draggable': False,
41
+ 'wrap': 'overflow',
42
+ 'placeholder': "Edit the plugin's description",
43
+ 'id': {'type': 'description-textarea', 'index': plugin_name},
44
+ }
40
45
 
41
46
  card_body_children = [html.H4(plugin_name)]
42
47
 
43
48
  if is_plugin_owner(plugin_name, session_data):
44
49
  desc_textarea_kw['readOnly'] = False
45
- card_body_children += [dbc.Textarea(**desc_textarea_kw)]
50
+ card_body_children.append(dbc.Textarea(**desc_textarea_kw))
46
51
  if not desc_textarea_kw['readOnly']:
47
52
  card_body_children += [
48
53
  dbc.Button(
@@ -53,12 +58,23 @@ def get_plugins_cards(
53
58
  ),
54
59
  html.Div(id={'type': 'edit-alert-div', 'index': plugin_name}),
55
60
  ]
56
- _plugin_username = get_api_connector().get_plugin_username(plugin, debug=debug)
61
+ plugin_username = get_api_connector().get_plugin_username(plugin, debug=debug)
62
+ plugin_version = get_api_connector().get_plugin_version(plugin, debug=debug) or ' '
57
63
  card_children = [
58
- dbc.CardHeader([html.A('👤 ' + str(_plugin_username), href='#')]),
64
+ dbc.CardHeader(
65
+ [
66
+ dbc.Row(
67
+ [
68
+ dbc.Col(html.A('👤 ' + str(plugin_username), href='#')),
69
+ dbc.Col(html.Pre(str(plugin_version), style={'text-align': 'right'})),
70
+ ],
71
+ justify = 'between',
72
+ ),
73
+ ],
74
+ ),
59
75
  dbc.CardBody(card_body_children),
60
76
  dbc.CardFooter([
61
- html.A('⬇️ Download source', href=(endpoints['plugins'] + '/' + plugin_name))
77
+ html.A('⬇️ Download', href=(endpoints['plugins'] + '/' + plugin_name))
62
78
  ]),
63
79
  ]
64
80
  cards.append(
@@ -70,6 +70,9 @@ window.addEventListener(
70
70
  if (!fl){ continue; }
71
71
  fl_val = event.data['input_flags_texts'][index];
72
72
  if (!fl_val){ continue; }
73
+ if (fl_val.includes(' ')){
74
+ fl_val = "'" + fl_val + "'";
75
+ }
73
76
  flags_str += " " + fl + " " + fl_val;
74
77
  }
75
78
 
@@ -39,10 +39,11 @@ def login(
39
39
  else (data.username, data.password)
40
40
  ) if not no_auth else ('no-auth', 'no-auth')
41
41
 
42
- from meerschaum.core.User._User import get_pwd_context
42
+ from meerschaum.core.User._User import verify_password
43
43
  user = User(username, password)
44
- correct_password = no_auth or get_pwd_context().verify(
45
- password, get_api_connector().get_user_password_hash(user, debug=debug)
44
+ correct_password = no_auth or verify_password(
45
+ password,
46
+ get_api_connector().get_user_password_hash(user, debug=debug)
46
47
  )
47
48
  if not correct_password:
48
49
  raise InvalidCredentialsException
@@ -51,7 +52,7 @@ def login(
51
52
  expires_delta = timedelta(minutes=expires_minutes)
52
53
  expires_dt = datetime.now(timezone.utc).replace(tzinfo=None) + expires_delta
53
54
  access_token = manager.create_access_token(
54
- data = dict(sub=username),
55
+ data = {'sub': username},
55
56
  expires = expires_delta
56
57
  )
57
58
  return {
@@ -90,18 +90,21 @@ def register_plugin(
90
90
  pass
91
91
 
92
92
  plugin = Plugin(name, version=version, attributes=attributes)
93
+ if curr_user is None:
94
+ return (
95
+ False,
96
+ "Cannot register a plugin without logging in (are you running with `--insecure`)?"
97
+ )
98
+
93
99
  if curr_user is not None:
94
100
  plugin_user_id = get_api_connector().get_plugin_user_id(plugin)
95
101
  curr_user_id = get_api_connector().get_user_id(curr_user) if curr_user is not None else -1
96
102
  if plugin_user_id is not None and plugin_user_id != curr_user_id:
97
103
  return False, f"User '{curr_user.username}' cannot edit plugin '{plugin}'."
98
104
  plugin.user_id = curr_user_id
99
- else:
100
- plugin.user_id = -1
101
105
 
102
106
  success, msg = get_api_connector().register_plugin(plugin, make_archive=False, debug=debug)
103
107
 
104
- ### TODO delete and install new version of plugin on success
105
108
  if success:
106
109
  archive_path = plugin.archive_path
107
110
  temp_archive_path = pathlib.Path(str(archive_path) + '.tmp')
@@ -0,0 +1,11 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Define default configuration for the Dash application.
7
+ """
8
+
9
+ default_dash_config = {
10
+ 'max_num_pipes_cards': 20,
11
+ }
@@ -44,6 +44,7 @@ default_meerschaum_config = {
44
44
  },
45
45
  'local': {
46
46
  'host': 'localhost',
47
+ 'port': 8000,
47
48
  },
48
49
  'mrsm': {
49
50
  'host': 'api.mrsm.io',
@@ -151,7 +152,6 @@ default_config['pipes'] = default_pipes_config
151
152
  default_config['plugins'] = default_plugins_config
152
153
  from meerschaum.config._jobs import default_jobs_config
153
154
  default_config['jobs'] = default_jobs_config
154
- # default_config['experimental'] = default_experimental_config
155
155
  ### add configs from other packages
156
156
  try:
157
157
  import meerschaum.config.stack
@@ -160,6 +160,8 @@ except ImportError as e:
160
160
  finally:
161
161
  from meerschaum.config.stack import default_stack_config
162
162
  default_config['stack'] = default_stack_config
163
+ from meerschaum.config._dash import default_dash_config
164
+ default_config['dash'] = default_dash_config
163
165
 
164
166
  default_header_comment = """
165
167
  #####################################################################
@@ -9,13 +9,19 @@ Default configuration for jobs.
9
9
  default_jobs_config = {
10
10
  'timeout_seconds': 8,
11
11
  'check_timeout_interval_seconds': 0.1,
12
- 'logs' : {
12
+ 'terminal': {
13
+ 'lines': 40,
14
+ 'columns': 70,
15
+ },
16
+ 'logs': {
13
17
  'num_files_to_keep': 5,
14
18
  'max_file_size': 100_000,
15
19
  'lines_to_show': 30,
16
- 'refresh_files_seconds': 5.0,
17
- 'min_buffer_len': 15,
18
- 'colors' : [
20
+ 'refresh_files_seconds': 5,
21
+ 'min_buffer_len': 10,
22
+ 'timestamp_format': '%Y-%m-%d %H:%M',
23
+ 'follow_timestamp_format': '%H:%M',
24
+ 'colors': [
19
25
  'cyan',
20
26
  'magenta',
21
27
  'orange3',
@@ -153,6 +153,7 @@ paths = {
153
153
 
154
154
  'DAEMON_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'jobs'),
155
155
  'LOGS_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'logs'),
156
+ 'DAEMON_ERROR_LOG_PATH' : ('{ROOT_DIR_PATH}', 'daemon_errors.log'),
156
157
  }
157
158
 
158
159
  def set_root(root: Union[Path, str]):
@@ -52,7 +52,7 @@ def sync_yaml_configs(
52
52
  if not path.exists():
53
53
  return "", {}
54
54
  header_comment = ""
55
- with open(path, 'r') as f:
55
+ with open(path, 'r', encoding='utf-8') as f:
56
56
  if _yaml is not None:
57
57
  config = yaml.load(f)
58
58
  else:
@@ -84,7 +84,7 @@ def sync_yaml_configs(
84
84
  new_path = sub_path
85
85
 
86
86
  ### write changes
87
- with open(new_path, 'w+') as f:
87
+ with open(new_path, 'w+', encoding='utf-8') as f:
88
88
  f.write(new_header)
89
89
  f.write(new_config_text)
90
90
  if permissions is not None:
@@ -133,4 +133,3 @@ def sync_files(keys: Optional[List[str]] = None):
133
133
  for k in keys:
134
134
  if k in key_functions:
135
135
  key_functions[k]()
136
-
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.2.0rc1"
5
+ __version__ = "2.2.0rc2"
@@ -33,7 +33,7 @@ api_host = "api"
33
33
 
34
34
  env_dict = {
35
35
  'COMPOSE_PROJECT_NAME' : 'mrsm',
36
- 'TIMESCALEDB_VERSION' : 'latest-pg15-oss',
36
+ 'TIMESCALEDB_VERSION' : 'latest-pg16-oss',
37
37
  'POSTGRES_USER' : f'{db_user}',
38
38
  'POSTGRES_PASSWORD' : f'{db_pass}',
39
39
  'POSTGRES_DB' : f'{db_base}',
@@ -232,11 +232,11 @@ NECESSARY_FILES = [STACK_COMPOSE_PATH, GRAFANA_DATASOURCE_PATH, GRAFANA_DASHBOAR
232
232
  def get_necessary_files():
233
233
  from meerschaum.config import get_config
234
234
  return {
235
- STACK_COMPOSE_PATH : (
235
+ STACK_COMPOSE_PATH: (
236
236
  get_config('stack', STACK_COMPOSE_FILENAME, substitute=True), compose_header
237
237
  ),
238
- GRAFANA_DATASOURCE_PATH : get_config('stack', 'grafana', 'datasource', substitute=True),
239
- GRAFANA_DASHBOARD_PATH : get_config('stack', 'grafana', 'dashboard', substitute=True),
238
+ GRAFANA_DATASOURCE_PATH: get_config('stack', 'grafana', 'datasource', substitute=True),
239
+ GRAFANA_DASHBOARD_PATH: get_config('stack', 'grafana', 'dashboard', substitute=True),
240
240
  }
241
241
 
242
242
 
@@ -250,8 +250,8 @@ def write_stack(
250
250
  return sync_files(['stack'])
251
251
 
252
252
  def edit_stack(
253
- action : Optional[List[str]] = None,
254
- debug : bool = False,
253
+ action: Optional[List[str]] = None,
254
+ debug: bool = False,
255
255
  **kw
256
256
  ):
257
257
  """Open docker-compose.yaml or .env for editing."""
@@ -17,7 +17,7 @@ default_datasource = {
17
17
  'type': 'postgres',
18
18
  'jsonData': {
19
19
  'sslmode': 'disable',
20
- 'postgresVersion': 1400,
20
+ 'postgresVersion': 1500,
21
21
  'timescaledb': True,
22
22
  },
23
23
  'user': db_user,
@@ -103,11 +103,13 @@ STATIC_CONFIG: Dict[str, Any] = {
103
103
  },
104
104
  'users': {
105
105
  'password_hash': {
106
+ 'algorithm_name': 'sha256',
107
+ 'salt_bytes': 16,
106
108
  'schemes': [
107
109
  'pbkdf2_sha256',
108
110
  ],
109
111
  'default': 'pbkdf2_sha256',
110
- 'pbkdf2_sha256__default_rounds': 30000,
112
+ 'pbkdf2_sha256__default_rounds': 3_000_000,
111
113
  },
112
114
  'min_username_length': 1,
113
115
  'max_username_length': 26,
@@ -108,9 +108,7 @@ def get_plugin_version(
108
108
  plugins_tbl = get_tables(mrsm_instance=self, debug=debug)['plugins']
109
109
  from meerschaum.utils.packages import attempt_import
110
110
  sqlalchemy = attempt_import('sqlalchemy')
111
-
112
111
  query = sqlalchemy.select(plugins_tbl.c.version).where(plugins_tbl.c.plugin_name == plugin.name)
113
-
114
112
  return self.value(query, debug=debug)
115
113
 
116
114
  def get_plugin_user_id(