meerschaum 2.2.0.dev3__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 (38) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/_internal/entry.py +1 -1
  3. meerschaum/actions/show.py +128 -42
  4. meerschaum/api/dash/callbacks/dashboard.py +2 -7
  5. meerschaum/api/dash/pipes.py +33 -9
  6. meerschaum/api/dash/plugins.py +25 -9
  7. meerschaum/api/resources/templates/termpage.html +3 -0
  8. meerschaum/api/routes/_login.py +5 -4
  9. meerschaum/api/routes/_plugins.py +6 -3
  10. meerschaum/config/_dash.py +11 -0
  11. meerschaum/config/_default.py +3 -1
  12. meerschaum/config/_jobs.py +10 -4
  13. meerschaum/config/_paths.py +1 -0
  14. meerschaum/config/_sync.py +2 -3
  15. meerschaum/config/_version.py +1 -1
  16. meerschaum/config/stack/__init__.py +6 -6
  17. meerschaum/config/stack/grafana/__init__.py +1 -1
  18. meerschaum/config/static/__init__.py +3 -1
  19. meerschaum/connectors/sql/_plugins.py +0 -2
  20. meerschaum/core/User/_User.py +156 -16
  21. meerschaum/core/User/__init__.py +1 -1
  22. meerschaum/plugins/_Plugin.py +1 -1
  23. meerschaum/utils/daemon/Daemon.py +63 -34
  24. meerschaum/utils/daemon/FileDescriptorInterceptor.py +102 -0
  25. meerschaum/utils/daemon/RotatingFile.py +120 -14
  26. meerschaum/utils/daemon/__init__.py +1 -0
  27. meerschaum/utils/packages/__init__.py +9 -2
  28. meerschaum/utils/packages/_packages.py +3 -3
  29. meerschaum/utils/schedule.py +41 -47
  30. meerschaum/utils/threading.py +1 -0
  31. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/METADATA +10 -9
  32. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/RECORD +38 -36
  33. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/WHEEL +1 -1
  34. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/LICENSE +0 -0
  35. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/NOTICE +0 -0
  36. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/entry_points.txt +0 -0
  37. {meerschaum-2.2.0.dev3.dist-info → meerschaum-2.2.0rc2.dist-info}/top_level.txt +0 -0
  38. {meerschaum-2.2.0.dev3.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.
@@ -49,7 +49,7 @@ def entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
49
49
 
50
50
  if args.get('schedule', None):
51
51
  from meerschaum.utils.schedule import schedule_function
52
- return schedule_function(entry_with_args, args['schedule'], **args)
52
+ return schedule_function(entry_with_args, **args)
53
53
  return entry_with_args(**args)
54
54
 
55
55
 
@@ -41,6 +41,7 @@ def show(
41
41
  'jobs' : _show_jobs,
42
42
  'logs' : _show_logs,
43
43
  'tags' : _show_tags,
44
+ 'schedules' : _show_schedules,
44
45
  }
45
46
  return choose_subaction(action, show_options, **kw)
46
47
 
@@ -577,6 +578,7 @@ def _show_logs(
577
578
  `show logs myjob myotherjob`
578
579
  """
579
580
  import os, pathlib, random, asyncio
581
+ from datetime import datetime, timezone
580
582
  from meerschaum.utils.packages import attempt_import, import_rich
581
583
  from meerschaum.utils.daemon import get_filtered_daemons, Daemon
582
584
  from meerschaum.utils.warnings import warn, info
@@ -587,72 +589,106 @@ def _show_logs(
587
589
  if not ANSI:
588
590
  info = print
589
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')
590
594
  daemons = get_filtered_daemons(action)
591
-
592
- def _build_buffer_spaces(daemons) -> Dict[str, str]:
593
- _max_len_id = max([len(d.daemon_id) for d in daemons]) if daemons else 0
594
- _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
+ )
595
605
  return {
596
- 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))])
597
607
  for d in daemons
598
608
  }
599
609
 
600
- 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]:
601
611
  return {d.daemon_id: colors[i % len(colors)] for i, d in enumerate(daemons)}
602
612
 
603
- _buffer_spaces = _build_buffer_spaces(daemons)
604
- _job_colors = _build_job_colors(daemons)
613
+ buffer_spaces = build_buffer_spaces(daemons)
614
+ job_colors = build_job_colors(daemons)
605
615
 
606
- def _get_buffer_spaces(daemon_id):
607
- nonlocal _buffer_spaces, daemons
608
- 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:
609
619
  d = Daemon(daemon_id=daemon_id)
610
620
  if d not in daemons:
611
621
  daemons = get_filtered_daemons(action)
612
- _buffer_spaces = _build_buffer_spaces(daemons)
613
- return _buffer_spaces[daemon_id]
622
+ buffer_spaces = build_buffer_spaces(daemons)
623
+ return buffer_spaces[daemon_id] or ' '
614
624
 
615
- def _get_job_colors(daemon_id):
616
- nonlocal _job_colors, daemons
617
- 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:
618
628
  d = Daemon(daemon_id=daemon_id)
619
629
  if d not in daemons:
620
630
  daemons = get_filtered_daemons(action)
621
- _job_colors = _build_job_colors(daemons)
622
- return _job_colors[daemon_id]
631
+ job_colors = build_job_colors(daemons)
632
+ return job_colors[daemon_id]
623
633
 
624
- def _follow_pretty_print():
625
- watchgod = attempt_import('watchgod')
634
+ def follow_pretty_print():
635
+ watchfiles = attempt_import('watchfiles')
626
636
  rich = import_rich()
627
637
  rich_text = attempt_import('rich.text')
628
- _watch_daemon_ids = {d.daemon_id: d for d in daemons}
638
+ watch_daemon_ids = {d.daemon_id: d for d in daemons}
629
639
  info("Watching log files...")
630
640
 
631
- def _print_job_line(daemon, line):
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)]
645
+ try:
646
+ line_timestamp = datetime.strptime(date_prefix_str, timestamp_format)
647
+ previous_line_timestamp = line_timestamp
648
+ except Exception as e:
649
+ line_timestamp = None
650
+ if line_timestamp:
651
+ line = line[(len(now_str) + 3):]
652
+ else:
653
+ line_timestamp = previous_line_timestamp
654
+
655
+ if len(line) == 0 or line == '\n':
656
+ return
657
+
632
658
  text = rich_text.Text(daemon.daemon_id)
633
- text.append(
634
- _get_buffer_spaces(daemon.daemon_id) + '| '
635
- + (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
+ + ' | '
636
663
  )
664
+ text.append(line_prefix + (line[:-1] if line[-1] == '\n' else line))
637
665
  if ANSI:
638
666
  text.stylize(
639
- _get_job_colors(daemon.daemon_id),
667
+ get_job_colors(daemon.daemon_id),
640
668
  0,
641
- len(daemon.daemon_id) + len(_get_buffer_spaces(daemon.daemon_id)) + 1
669
+ len(daemon.daemon_id) + len(line_prefix)
642
670
  )
643
671
  get_console().print(text)
644
672
 
645
673
 
646
- def _print_log_lines(daemon):
674
+ def print_log_lines(daemon):
647
675
  for line in daemon.readlines():
648
- _print_job_line(daemon, line)
676
+ print_job_line(daemon, line)
649
677
 
650
- def _seek_back_offset(d) -> bool:
678
+ def seek_back_offset(d) -> bool:
651
679
  if d.log_offset_path.exists():
652
680
  d.log_offset_path.unlink()
653
681
 
654
682
  latest_subfile_path = d.rotating_log.get_latest_subfile_path()
655
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
+
656
692
  with open(latest_subfile_path, 'r', encoding='utf-8') as f:
657
693
  latest_lines = f.readlines()
658
694
 
@@ -672,12 +708,12 @@ def _show_logs(
672
708
  for d in daemons
673
709
  }
674
710
  for d in daemons:
675
- _seek_back_offset(d)
676
- _print_log_lines(d)
711
+ seek_back_offset(d)
712
+ print_log_lines(d)
677
713
 
678
714
  _quit = False
679
- async def _watch_logs():
680
- async for changes in watchgod.awatch(LOGS_RESOURCES_PATH):
715
+ def watch_logs():
716
+ for changes in watchfiles.watch(LOGS_RESOURCES_PATH):
681
717
  if _quit:
682
718
  return
683
719
  for change in changes:
@@ -688,7 +724,7 @@ def _show_logs(
688
724
  if not file_path.exists():
689
725
  continue
690
726
  daemon_id = file_path.name.split('.log')[0]
691
- if daemon_id not in _watch_daemon_ids and action:
727
+ if daemon_id not in watch_daemon_ids and action:
692
728
  continue
693
729
  try:
694
730
  daemon = Daemon(daemon_id=daemon_id)
@@ -697,22 +733,21 @@ def _show_logs(
697
733
  warn(f"Seeing new logs for non-existent job '{daemon_id}'.", stack=False)
698
734
 
699
735
  if daemon is not None:
700
- _print_log_lines(daemon)
736
+ print_log_lines(daemon)
701
737
 
702
- loop = asyncio.new_event_loop()
703
738
  try:
704
- loop.run_until_complete(_watch_logs())
705
- except KeyboardInterrupt:
739
+ watch_logs()
740
+ except KeyboardInterrupt as ki:
706
741
  _quit = True
707
742
 
708
- def _print_nopretty_log_text():
743
+ def print_nopretty_log_text():
709
744
  for d in daemons:
710
745
  log_text = d.log_text
711
746
  print(d.daemon_id)
712
747
  print(log_text)
713
748
 
714
- _print_log_text = _follow_pretty_print if not nopretty else _print_nopretty_log_text
715
- _print_log_text()
749
+ print_log_text = follow_pretty_print if not nopretty else print_nopretty_log_text
750
+ print_log_text()
716
751
 
717
752
  return True, "Success"
718
753
 
@@ -817,6 +852,57 @@ def _show_tags(
817
852
  return True, "Success"
818
853
 
819
854
 
855
+ def _show_schedules(
856
+ action: Optional[List[str]] = None,
857
+ nopretty: bool = False,
858
+ **kwargs: Any
859
+ ) -> SuccessTuple:
860
+ """
861
+ Print the upcoming timestamps according to the given schedule.
862
+
863
+ Examples:
864
+ show schedule 'daily starting 00:00'
865
+ show schedule 'every 12 hours and mon-fri starting 2024-01-01'
866
+ """
867
+ from meerschaum.utils.schedule import parse_schedule
868
+ from meerschaum.utils.misc import is_int
869
+ from meerschaum.utils.formatting import print_options
870
+ if not action:
871
+ return False, "Provide a schedule to be parsed."
872
+ schedule = action[0]
873
+ default_num_timestamps = 5
874
+ num_timestamps_str = action[1] if len(action) >= 2 else str(default_num_timestamps)
875
+ num_timestamps = (
876
+ int(num_timestamps_str)
877
+ if is_int(num_timestamps_str)
878
+ else default_num_timestamps
879
+ )
880
+ try:
881
+ trigger = parse_schedule(schedule)
882
+ except ValueError as e:
883
+ return False, str(e)
884
+
885
+ next_datetimes = []
886
+ for _ in range(num_timestamps):
887
+ try:
888
+ next_dt = trigger.next()
889
+ next_datetimes.append(next_dt)
890
+ except Exception as e:
891
+ break
892
+
893
+ print_options(
894
+ next_datetimes,
895
+ num_cols = 1,
896
+ nopretty = nopretty,
897
+ header = (
898
+ f"Next {min(num_timestamps, len(next_datetimes))} timestamps "
899
+ + f"for schedule '{schedule}':"
900
+ ),
901
+ )
902
+
903
+ return True, "Success"
904
+
905
+
820
906
 
821
907
  ### NOTE: This must be the final statement of the module.
822
908
  ### Any subactions added below these lines will not
@@ -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.0.dev3"
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,