meerschaum 3.0.0rc4__py3-none-any.whl → 3.0.0rc7__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 +434 -0
  6. meerschaum/_internal/docs/index.py +1 -2
  7. meerschaum/_internal/entry.py +44 -8
  8. meerschaum/_internal/shell/Shell.py +113 -19
  9. meerschaum/_internal/shell/__init__.py +4 -1
  10. meerschaum/_internal/static.py +3 -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 +20 -10
  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 +135 -35
  85. meerschaum/jobs/systemd.py +7 -2
  86. meerschaum/plugins/__init__.py +277 -81
  87. meerschaum/utils/daemon/Daemon.py +195 -41
  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 +16 -6
  99. meerschaum/utils/misc.py +18 -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 +171 -144
  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.0rc7.dist-info}/METADATA +3 -1
  110. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/RECORD +116 -110
  111. meerschaum/config/_environment.py +0 -145
  112. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/WHEEL +0 -0
  113. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/entry_points.txt +0 -0
  114. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/LICENSE +0 -0
  115. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/licenses/NOTICE +0 -0
  116. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/top_level.txt +0 -0
  117. {meerschaum-3.0.0rc4.dist-info → meerschaum-3.0.0rc7.dist-info}/zip-safe +0 -0
@@ -21,7 +21,6 @@ from meerschaum.config._paths import API_UVICORN_CONFIG_PATH, API_UVICORN_RESOUR
21
21
  from meerschaum.plugins import _api_plugins
22
22
  from meerschaum.utils.warnings import warn, dprint
23
23
  from meerschaum.utils.threading import RLock
24
- from meerschaum.utils.misc import is_pipe_registered
25
24
  from meerschaum.connectors.parse import parse_instance_keys
26
25
 
27
26
  from meerschaum import __version__ as version
@@ -67,16 +66,13 @@ from meerschaum.api._exceptions import APIPermissionError
67
66
  uvicorn_config_path = API_UVICORN_RESOURCES_PATH / SERVER_ID / 'config.json'
68
67
 
69
68
  uvicorn_config = None
70
- sys_config = get_config('system', 'api')
71
- permissions_config = get_config('system', 'api', 'permissions')
69
+ sys_config = get_config('api')
70
+ permissions_config = get_config('api', 'permissions')
72
71
 
73
72
  def get_uvicorn_config() -> Dict[str, Any]:
74
73
  """Read the Uvicorn configuration JSON and return a dictionary."""
75
74
  global uvicorn_config
76
75
  import json
77
- runtime = os.environ.get(STATIC_CONFIG['environment']['runtime'], None)
78
- if runtime == 'api':
79
- return get_config('system', 'api', 'uvicorn')
80
76
  _uvicorn_config = uvicorn_config
81
77
  with _locks['uvicorn_config']:
82
78
  if uvicorn_config is None:
@@ -85,6 +81,8 @@ def get_uvicorn_config() -> Dict[str, Any]:
85
81
  uvicorn_config = json.load(f)
86
82
  _uvicorn_config = uvicorn_config
87
83
  except Exception:
84
+ import traceback
85
+ traceback.print_exc()
88
86
  _uvicorn_config = sys_config.get('uvicorn', None)
89
87
 
90
88
  if _uvicorn_config is None:
@@ -102,6 +100,10 @@ production = get_uvicorn_config().get('production', False)
102
100
  _include_dash = (not no_dash)
103
101
  _include_webterm = (not no_webterm) and _include_dash
104
102
  docs_enabled = not production or sys_config.get('endpoints', {}).get('docs_in_production', True)
103
+ webterm_port = (
104
+ get_uvicorn_config().get('webterm_port', None)
105
+ or mrsm.get_config('api', 'webterm', 'port')
106
+ )
105
107
 
106
108
  default_instance_keys = None
107
109
  _instance_connectors = defaultdict(lambda: None)
@@ -129,7 +131,7 @@ def get_api_connector(instance_keys: Optional[str] = None):
129
131
  )
130
132
  found_match: bool = False
131
133
  for allowed_keys_pattern in allowed_instance_keys:
132
- if fnmatch(instance_keys, allowed_keys_pattern):
134
+ if fnmatch(str(instance_keys), allowed_keys_pattern):
133
135
  found_match = True
134
136
  break
135
137
  if not found_match:
@@ -141,7 +143,9 @@ def get_api_connector(instance_keys: Optional[str] = None):
141
143
  if _instance_connectors[instance_keys] is None:
142
144
  try:
143
145
  is_valid_connector = True
144
- _instance_connectors[instance_keys] = parse_instance_keys(instance_keys, debug=debug)
146
+ instance_connector = parse_instance_keys(instance_keys, debug=debug)
147
+ instance_connector._cache_connector = get_cache_connector()
148
+ _instance_connectors[instance_keys] = instance_connector
145
149
  except Exception:
146
150
  is_valid_connector = False
147
151
 
@@ -168,7 +172,7 @@ def get_cache_connector(connector_keys: Optional[str] = None):
168
172
  return None
169
173
 
170
174
  connector_keys = connector_keys or get_config(
171
- 'system', 'api', 'cache', 'connector',
175
+ 'api', 'cache', 'connector',
172
176
  warn=False,
173
177
  )
174
178
  if connector_keys is None:
@@ -196,7 +200,11 @@ def pipes(instance_keys: Optional[str] = None, refresh: bool = False) -> PipesDi
196
200
  with _locks['pipes-' + instance_keys]:
197
201
  pipes = _instance_pipes[instance_keys]
198
202
  if pipes is None or refresh:
199
- pipes = _get_pipes(mrsm_instance=instance_keys)
203
+ pipes = _get_pipes(
204
+ mrsm_instance=instance_keys,
205
+ cache=True,
206
+ cache_connector_keys=get_cache_connector(),
207
+ )
200
208
  _instance_pipes[instance_keys] = pipes
201
209
  return pipes
202
210
 
@@ -218,9 +226,23 @@ def get_pipe(
218
226
  detail="Unable to serve any pipes with connector keys `mrsm` over the API.",
219
227
  )
220
228
 
221
- pipe = mrsm.Pipe(connector_keys, metric_key, location_key, mrsm_instance=instance_keys)
222
- if is_pipe_registered(pipe, pipes(instance_keys, refresh=False)):
223
- return pipes(instance_keys, refresh=refresh)[connector_keys][metric_key][location_key]
229
+ pipes_dict = pipes(instance_keys)
230
+ if (
231
+ not refresh
232
+ and connector_keys in pipes_dict
233
+ and metric_key in pipes_dict[connector_keys]
234
+ and location_key in pipes_dict[connector_keys][metric_key]
235
+ ):
236
+ return pipes_dict[connector_keys][metric_key][location_key]
237
+
238
+ pipe = mrsm.Pipe(
239
+ connector_keys,
240
+ metric_key,
241
+ location_key,
242
+ mrsm_instance=instance_keys,
243
+ cache=True,
244
+ cache_connector_keys=get_cache_connector(),
245
+ )
224
246
  return pipe
225
247
 
226
248
 
meerschaum/api/_events.py CHANGED
@@ -8,13 +8,13 @@ Declare FastAPI events in this module (startup, shutdown, etc.).
8
8
 
9
9
  import sys
10
10
  import os
11
- import time
12
11
  from meerschaum.api import (
13
12
  app,
14
13
  get_api_connector,
15
14
  get_cache_connector,
16
15
  get_uvicorn_config,
17
16
  debug,
17
+ webterm_port,
18
18
  no_dash,
19
19
  _include_dash,
20
20
  _include_webterm,
@@ -75,7 +75,7 @@ async def startup():
75
75
  try:
76
76
  if _include_webterm:
77
77
  from meerschaum.api.dash.webterm import start_webterm
78
- start_webterm()
78
+ start_webterm(webterm_port=webterm_port)
79
79
 
80
80
  connected = retry_connect(
81
81
  get_api_connector(),
meerschaum/api/_oauth2.py CHANGED
@@ -21,6 +21,7 @@ from meerschaum.core import User, Token
21
21
  fastapi, starlette = attempt_import('fastapi', 'starlette', lazy=False, check_update=CHECK_UPDATE)
22
22
  fastapi_responses = attempt_import('fastapi.responses', lazy=False, check_update=CHECK_UPDATE)
23
23
  fastapi_login = attempt_import('fastapi_login', check_update=CHECK_UPDATE)
24
+ jose_jwt, jose_exceptions = attempt_import('jose.jwt', 'jose.exceptions', lazy=False, check_update=CHECK_UPDATE)
24
25
  from fastapi import Depends, HTTPException, Request
25
26
  from starlette import status
26
27
 
@@ -91,6 +92,7 @@ async def load_user_or_token(
91
92
  status_code=status.HTTP_401_UNAUTHORIZED,
92
93
  detail="Not authenticated.",
93
94
  )
95
+
94
96
  authorization = authorization.replace('Basic ', '').replace('Bearer ', '')
95
97
  if not authorization.startswith('mrsm-key:'):
96
98
  if not users:
@@ -98,12 +100,15 @@ async def load_user_or_token(
98
100
  status=status.HTTP_401_UNAUTHORIZED,
99
101
  detail="Users not authenticated for this endpoint.",
100
102
  )
103
+
101
104
  return await manager(request)
105
+
102
106
  if not tokens:
103
107
  raise HTTPException(
104
108
  status_code=status.HTTP_401_UNAUTHORIZED,
105
109
  detail="Tokens not authenticated for this endpoint.",
106
110
  )
111
+
107
112
  return get_token_from_authorization(authorization)
108
113
 
109
114
 
@@ -112,6 +117,7 @@ def ScopedAuth(scopes: List[str]):
112
117
  Dependency factory for authenticating with either a user session or a scoped token.
113
118
  """
114
119
  async def _authenticate(
120
+ request: Request,
115
121
  user_or_token: Union[User, Token, None] = Depends(
116
122
  load_user_or_token,
117
123
  ),
@@ -126,12 +132,45 @@ def ScopedAuth(scopes: List[str]):
126
132
  headers={"WWW-Authenticate": "Basic"},
127
133
  )
128
134
 
129
- fresh_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
130
- if '*' in fresh_scopes:
135
+ authorization = request.headers.get('authorization', request.headers.get('Authorization', None))
136
+ is_long_lived = authorization and 'mrsm-key:' in authorization
137
+
138
+ current_scopes = []
139
+ ### For long-lived API tokens, always hit the database.
140
+ if is_long_lived:
141
+ current_scopes = user_or_token.get_scopes(refresh=True, debug=debug)
142
+
143
+ ### For JWTs, trust the scopes in the token.
144
+ else:
145
+ if not authorization:
146
+ # This should be caught by `load_user_or_token` but we can be safe.
147
+ raise HTTPException(
148
+ status_code=status.HTTP_401_UNAUTHORIZED,
149
+ detail="Not authenticated.",
150
+ )
151
+
152
+ scheme, _, token_str = authorization.partition(' ')
153
+ if not token_str or scheme.lower() != 'bearer':
154
+ raise HTTPException(
155
+ status_code=status.HTTP_401_UNAUTHORIZED,
156
+ detail="Unsupported authentication scheme.",
157
+ )
158
+
159
+ try:
160
+ payload = jose_jwt.decode(token_str, SECRET, algorithms=['HS256'])
161
+ current_scopes = payload.get('scopes', [])
162
+ except jose_exceptions.JWTError:
163
+ raise HTTPException(
164
+ status_code=status.HTTP_401_UNAUTHORIZED,
165
+ detail="Invalid access token.",
166
+ headers={"WWW-Authenticate": "Bearer"},
167
+ )
168
+
169
+ if '*' in current_scopes:
131
170
  return user_or_token
132
171
 
133
172
  for scope in scopes:
134
- if scope not in fresh_scopes:
173
+ if scope not in current_scopes:
135
174
  raise HTTPException(
136
175
  status_code=status.HTTP_403_FORBIDDEN,
137
176
  detail=f"Missing required scope: '{scope}'",
@@ -159,4 +198,8 @@ def generate_secret_key() -> bytes:
159
198
 
160
199
 
161
200
  SECRET = generate_secret_key()
162
- manager = LoginManager(SECRET, token_url=endpoints['login'])
201
+ manager = LoginManager(
202
+ SECRET,
203
+ token_url=endpoints['login'],
204
+ scopes=STATIC_CONFIG['tokens']['scopes'],
205
+ )
@@ -1193,3 +1193,32 @@ def toggle_pages_offcanvas(n_clicks: Optional[int], is_open: bool):
1193
1193
  if n_clicks:
1194
1194
  return not is_open, pages_children
1195
1195
  return is_open, pages_children
1196
+
1197
+
1198
+ @dash_app.callback(
1199
+ Output({'type': 'calculate-rowcount-div', 'index': MATCH}, 'children'),
1200
+ Input({'type': 'calculate-rowcount-button', 'index': MATCH}, 'n_clicks'),
1201
+ prevent_initial_call=True,
1202
+ )
1203
+ def calculate_rowcount_button_click(n_clicks: int):
1204
+ """
1205
+ Calculate the rowcount for the pipe.
1206
+ """
1207
+ if not n_clicks:
1208
+ raise PreventUpdate
1209
+
1210
+ triggered = dash.callback_context.triggered
1211
+ if triggered[0]['value'] is None:
1212
+ raise PreventUpdate
1213
+
1214
+ pipe = pipe_from_ctx(triggered, 'n_clicks')
1215
+ if pipe is None:
1216
+ raise PreventUpdate
1217
+
1218
+ try:
1219
+ rowcount = pipe.get_rowcount(debug=debug)
1220
+ return f"{rowcount:,}"
1221
+ except Exception as e:
1222
+ return (
1223
+ alert_from_success_tuple((False, f"Failed to calculate row count: {e}"))
1224
+ )
@@ -13,6 +13,7 @@ import time
13
13
  import traceback
14
14
  from datetime import datetime, timezone
15
15
 
16
+ import meerschaum as mrsm
16
17
  from meerschaum.jobs import get_jobs
17
18
  from meerschaum.utils.typing import Optional, Dict, Any
18
19
  from meerschaum.api import CHECK_UPDATE
@@ -136,12 +137,12 @@ def manage_job_button_click(
136
137
  old_status = job.status
137
138
  try:
138
139
  success, msg = manage_functions[manage_job_action]()
139
- except Exception as e:
140
+ except Exception:
140
141
  success, msg = False, traceback.format_exc()
141
142
 
142
143
  ### Wait for a status change before building the elements.
143
144
  timeout_seconds = 1.0
144
- check_interval_seconds = 0.01
145
+ check_interval_seconds = mrsm.get_config('system', 'cli', 'refresh_seconds')
145
146
  begin = time.perf_counter()
146
147
  while (time.perf_counter() - begin) < timeout_seconds:
147
148
  if job.status != old_status:
@@ -16,11 +16,14 @@ from meerschaum.utils.typing import Optional
16
16
  dash = attempt_import('dash', lazy=False, check_update=CHECK_UPDATE)
17
17
  from dash.exceptions import PreventUpdate
18
18
  from dash.dependencies import Input, Output, State
19
+
19
20
  from meerschaum.api.dash import dash_app, debug, pipes, _get_pipes
20
21
  from meerschaum.api.dash.sessions import set_session
21
22
  from meerschaum.api.dash.connectors import get_web_connector
22
23
  from meerschaum.api.routes._login import login
23
24
  from meerschaum.api.dash.components import alert_from_success_tuple
25
+ from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
26
+ from meerschaum._internal.static import STATIC_CONFIG
24
27
  from fastapi_login.exceptions import InvalidCredentialsException
25
28
  from fastapi.exceptions import HTTPException
26
29
  dbc = attempt_import('dash_bootstrap_components', lazy=False, check_update=CHECK_UPDATE)
@@ -73,7 +76,13 @@ def login_button_click(
73
76
  raise PreventUpdate
74
77
 
75
78
  try:
76
- _ = login({'username': username, 'password': password})
79
+ form = CustomOAuth2PasswordRequestForm(
80
+ grant_type='password',
81
+ username=username,
82
+ password=password,
83
+ scope=' '.join(STATIC_CONFIG['tokens']['scopes'])
84
+ )
85
+ _ = login(form)
77
86
  session_id = str(uuid.uuid4())
78
87
  session_data = {
79
88
  'session-id': session_id,
@@ -15,6 +15,7 @@ from dash.exceptions import PreventUpdate
15
15
  from meerschaum.core import User
16
16
  from meerschaum._internal.static import STATIC_CONFIG
17
17
  from meerschaum.utils.packages import attempt_import
18
+ from meerschaum.api._oauth2 import CustomOAuth2PasswordRequestForm
18
19
  dash = attempt_import('dash', check_update=CHECK_UPDATE)
19
20
  from fastapi.exceptions import HTTPException
20
21
 
@@ -97,10 +98,16 @@ def register_button_click(
97
98
  form_class += ' is-invalid'
98
99
  return {}, form_class, dash.no_update
99
100
  try:
100
- _ = login({'username': username, 'password': password})
101
+ form = CustomOAuth2PasswordRequestForm(
102
+ grant_type='password',
103
+ username=username,
104
+ password=password,
105
+ scope=' '.join(STATIC_CONFIG['tokens']['scopes'])
106
+ )
107
+ _ = login(form)
101
108
  session_data = {'session-id': str(uuid.uuid4())}
102
109
  set_session(session_data['session-id'], {'username': username})
103
- except HTTPException as e:
110
+ except HTTPException:
104
111
  form_class += ' is-invalid'
105
112
  session_data = None
106
113
  return session_data, form_class, (dash.no_update if not session_data else endpoints['dash'])
@@ -49,11 +49,11 @@ registration_div = html.Div(
49
49
  html.Code('mrsm register user newuser -i sql:main', className='codeblock'),
50
50
  ),
51
51
  dcc.Markdown("""
52
- To enable online registration, open the `system` configuration file and""" +
52
+ To enable online registration, open the `api` configuration file and""" +
53
53
  """ set the permissions to `true`:"""
54
54
  ),
55
55
  html.Pre(
56
- html.Code('mrsm edit config system', className='codeblock'),
56
+ html.Code('mrsm edit config api', className='codeblock'),
57
57
  ),
58
58
  html.Br(),
59
59
  dcc.Markdown('The settings file should look something like this:'),
@@ -377,48 +377,48 @@ def accordion_items_from_pipe(
377
377
  html.Th(
378
378
  html.Span(
379
379
  "Key",
380
- id={'type': 'key-table-header', 'id': pipe_meta_str},
380
+ id={'type': 'key-table-header', 'index': pipe_meta_str},
381
381
  style={"textDecoration": "underline", "cursor": "pointer"},
382
382
  ),
383
383
  ),
384
384
  html.Th(
385
385
  html.Span(
386
386
  "Column",
387
- id={'type': 'column-table-header', 'id': pipe_meta_str},
387
+ id={'type': 'column-table-header', 'index': pipe_meta_str},
388
388
  style={"textDecoration": "underline", "cursor": "pointer"},
389
389
  ),
390
390
  ),
391
391
  html.Th(
392
392
  html.Span(
393
393
  "Index",
394
- id={'type': 'index-table-header', 'id': pipe_meta_str},
394
+ id={'type': 'index-table-header', 'index': pipe_meta_str},
395
395
  style={"textDecoration": "underline", "cursor": "pointer"},
396
396
  ),
397
397
  ),
398
398
  html.Th(
399
399
  html.Span(
400
400
  "Is Composite",
401
- id={'type': 'is-composite-table-header', 'id': pipe_meta_str},
401
+ id={'type': 'is-composite-table-header', 'index': pipe_meta_str},
402
402
  style={"textDecoration": "underline", "cursor": "pointer"},
403
403
  ),
404
404
  ),
405
405
  dbc.Tooltip(
406
406
  "Unique reference name for the index "
407
407
  "(e.g. `datetime` for the range axis)",
408
- target={'type': 'key-table-header', 'id': pipe_meta_str},
408
+ target={'type': 'key-table-header', 'index': pipe_meta_str},
409
409
  ),
410
410
  dbc.Tooltip(
411
411
  "The actual column (field name) in the target dataset.",
412
- target={'type': 'column-table-header', 'id': pipe_meta_str},
412
+ target={'type': 'column-table-header', 'index': pipe_meta_str},
413
413
  ),
414
414
  dbc.Tooltip(
415
415
  "The name of the index created on the given columns.",
416
- target={'type': 'index-table-header', 'id': pipe_meta_str},
416
+ target={'type': 'index-table-header', 'index': pipe_meta_str},
417
417
  ),
418
418
  dbc.Tooltip(
419
419
  "Whether the column is used in the composite primary key "
420
420
  "to determine updates.",
421
- target={'type': 'is-composite-table-header', 'id': pipe_meta_str},
421
+ target={'type': 'is-composite-table-header', 'index': pipe_meta_str},
422
422
  ),
423
423
  ]
424
424
  )
@@ -483,9 +483,14 @@ def accordion_items_from_pipe(
483
483
  ])
484
484
  )
485
485
 
486
- items_bodies['overview'] = dbc.Table(
487
- overview_header + [html.Tbody(overview_rows)],
488
- bordered=False, hover=True, striped=False,
486
+ items_bodies['overview'] = html.Div(
487
+ dbc.Table(
488
+ overview_header + [html.Tbody(overview_rows)],
489
+ bordered=False,
490
+ hover=True,
491
+ striped=False,
492
+ ),
493
+ style={'overflowX': 'auto'},
489
494
  )
490
495
 
491
496
  if 'stats' in active_items:
@@ -497,17 +502,13 @@ def accordion_items_from_pipe(
497
502
  (newest_time - oldest_time) if newest_time is not None and oldest_time is not None
498
503
  else None
499
504
  )
500
- rowcount = pipe.get_rowcount(debug=debug)
501
505
  except Exception:
502
506
  oldest_time = None
503
507
  newest_time = None
504
508
  interval = None
505
- rowcount = None
506
509
 
507
510
  stats_rows = []
508
- if rowcount is not None:
509
- stats_rows.append(html.Tr([html.Td("Row Count"), html.Td(f"{rowcount:,}")]))
510
- if interval is not None:
511
+ if interval is not None and not isinstance(interval, int):
511
512
  stats_rows.append(
512
513
  html.Tr([html.Td("Timespan"), html.Td(humanfriendly.format_timespan(interval))])
513
514
  )
@@ -516,7 +517,39 @@ def accordion_items_from_pipe(
516
517
  if newest_time is not None:
517
518
  stats_rows.append(html.Tr([html.Td("Newest time"), html.Td(str(newest_time))]))
518
519
 
519
- items_bodies['stats'] = dbc.Table(stats_header + [html.Tbody(stats_rows)], hover=True)
520
+ precision = pipe.precision
521
+ if precision:
522
+ stats_rows.append(
523
+ html.Tr([
524
+ html.Td("Precision"),
525
+ html.Td(str(precision.get('interval', 1)) + ' ' + str(precision.get('unit', 'unit')))
526
+ ])
527
+ )
528
+
529
+ stats_rows.append(
530
+ html.Tr([
531
+ html.Td("Row count"),
532
+ html.Td(
533
+ html.Div(
534
+ dbc.Button(
535
+ "Calculate",
536
+ color='link',
537
+ size='sm',
538
+ style={'text-decoration': 'none'},
539
+ id={'type': 'calculate-rowcount-button', 'index': pipe_meta_str},
540
+ )
541
+ if pipe.exists(debug=debug)
542
+ else '0'
543
+ ),
544
+ id={'type': 'calculate-rowcount-div', 'index': pipe_meta_str},
545
+ )
546
+ ])
547
+ )
548
+
549
+ items_bodies['stats'] = html.Div(
550
+ dbc.Table(stats_header + [html.Tbody(stats_rows)], hover=True),
551
+ style={'overflowX': 'auto'},
552
+ )
520
553
 
521
554
  if 'columns' in active_items:
522
555
  try:
@@ -546,7 +579,7 @@ def accordion_items_from_pipe(
546
579
  mode='norm',
547
580
  tabSize=4,
548
581
  theme='twilight',
549
- id={'type': 'parameters-editor', 'index': json.dumps(pipe.meta)},
582
+ id={'type': 'parameters-editor', 'index': pipe_meta_str},
550
583
  width='100%',
551
584
  height='500px',
552
585
  readOnly=False,
@@ -558,19 +591,19 @@ def accordion_items_from_pipe(
558
591
  )
559
592
  update_parameters_button = dbc.Button(
560
593
  "Update",
561
- id={'type': 'update-parameters-button', 'index': json.dumps(pipe.meta)},
594
+ id={'type': 'update-parameters-button', 'index': pipe_meta_str},
562
595
  )
563
596
 
564
597
  as_yaml_button = dbc.Button(
565
598
  "YAML",
566
- id={'type': 'parameters-as-yaml-button', 'index': json.dumps(pipe.meta)},
599
+ id={'type': 'parameters-as-yaml-button', 'index': pipe_meta_str},
567
600
  color='link',
568
601
  size='sm',
569
602
  style={'text-decoration': 'none'},
570
603
  )
571
604
  as_json_button = dbc.Button(
572
605
  "JSON",
573
- id={'type': 'parameters-as-json-button', 'index': json.dumps(pipe.meta)},
606
+ id={'type': 'parameters-as-json-button', 'index': pipe_meta_str},
574
607
  color='link',
575
608
  size='sm',
576
609
  style={'text-decoration': 'none', 'margin-left': '10px'},
@@ -596,7 +629,7 @@ def accordion_items_from_pipe(
596
629
  html.Div(
597
630
  id={
598
631
  'type': 'update-parameters-success-div',
599
- 'index': json.dumps(pipe.meta),
632
+ 'index': pipe_meta_str,
600
633
  }
601
634
  )
602
635
  ],
@@ -640,7 +673,7 @@ def accordion_items_from_pipe(
640
673
  mode='sql',
641
674
  tabSize=4,
642
675
  theme='twilight',
643
- id={'type': 'sql-editor', 'index': json.dumps(pipe.meta)},
676
+ id={'type': 'sql-editor', 'index': pipe_meta_str},
644
677
  width='100%',
645
678
  height='500px',
646
679
  readOnly=False,
@@ -652,7 +685,7 @@ def accordion_items_from_pipe(
652
685
  )
653
686
  update_sql_button = dbc.Button(
654
687
  "Update",
655
- id={'type': 'update-sql-button', 'index': json.dumps(pipe.meta)},
688
+ id={'type': 'update-sql-button', 'index': pipe_meta_str},
656
689
  )
657
690
  items_bodies['sql'] = html.Div([
658
691
  sql_editor,
@@ -661,7 +694,7 @@ def accordion_items_from_pipe(
661
694
  dbc.Col([update_sql_button], width=2),
662
695
  dbc.Col([
663
696
  html.Div(
664
- id={'type': 'update-sql-success-div', 'index': json.dumps(pipe.meta)}
697
+ id={'type': 'update-sql-success-div', 'index': pipe_meta_str}
665
698
  )
666
699
  ],
667
700
  width=True,
@@ -683,7 +716,7 @@ def accordion_items_from_pipe(
683
716
  mode='norm',
684
717
  tabSize=4,
685
718
  theme='twilight',
686
- id={'type': 'query-editor', 'index': json.dumps(pipe.meta)},
719
+ id={'type': 'query-editor', 'index': pipe_meta_str},
687
720
  width='100%',
688
721
  height='200px',
689
722
  readOnly=False,
@@ -695,17 +728,17 @@ def accordion_items_from_pipe(
695
728
  )
696
729
  query_data_button = dbc.Button(
697
730
  "Query",
698
- id={'type': 'query-data-button', 'index': json.dumps(pipe.meta)},
731
+ id={'type': 'query-data-button', 'index': pipe_meta_str},
699
732
  )
700
733
 
701
734
  begin_end_input_group = dbc.InputGroup(
702
735
  [
703
736
  dbc.Input(
704
- id={'type': 'query-data-begin-input', 'index': json.dumps(pipe.meta)},
737
+ id={'type': 'query-data-begin-input', 'index': pipe_meta_str},
705
738
  placeholder="Begin",
706
739
  ),
707
740
  dbc.Input(
708
- id={'type': 'query-data-end-input', 'index': json.dumps(pipe.meta)},
741
+ id={'type': 'query-data-end-input', 'index': pipe_meta_str},
709
742
  placeholder="End",
710
743
  ),
711
744
  ],
@@ -719,9 +752,12 @@ def accordion_items_from_pipe(
719
752
  value=10,
720
753
  step=1,
721
754
  placeholder="Limit",
722
- id={'type': 'limit-input', 'index': json.dumps(pipe.meta)},
755
+ id={'type': 'limit-input', 'index': pipe_meta_str},
756
+ )
757
+ query_result_div = html.Div(
758
+ id={'type': 'query-result-div', 'index': pipe_meta_str},
759
+ style={'overflowX': 'auto'},
723
760
  )
724
- query_result_div = html.Div(id={'type': 'query-result-div', 'index': json.dumps(pipe.meta)})
725
761
 
726
762
  items_bodies['query-data'] = html.Div([
727
763
  query_editor,
@@ -746,7 +782,7 @@ def accordion_items_from_pipe(
746
782
  mode = 'norm',
747
783
  tabSize = 4,
748
784
  theme = 'twilight',
749
- id = {'type': 'sync-editor', 'index': json.dumps(pipe.meta)},
785
+ id = {'type': 'sync-editor', 'index': pipe_meta_str},
750
786
  width = '100%',
751
787
  height = '500px',
752
788
  readOnly = False,
@@ -759,14 +795,14 @@ def accordion_items_from_pipe(
759
795
 
760
796
  sync_as_json_button = dbc.Button(
761
797
  "JSON",
762
- id={'type': 'sync-as-json-button', 'index': json.dumps(pipe.meta)},
798
+ id={'type': 'sync-as-json-button', 'index': pipe_meta_str},
763
799
  color='link',
764
800
  size='sm',
765
801
  style={'text-decoration': 'none', 'margin-left': '10px'},
766
802
  )
767
803
  sync_as_lines_button = dbc.Button(
768
804
  "Lines",
769
- id={'type': 'sync-as-lines-button', 'index': json.dumps(pipe.meta)},
805
+ id={'type': 'sync-as-lines-button', 'index': pipe_meta_str},
770
806
  color='link',
771
807
  size='sm',
772
808
  style={'text-decoration': 'none', 'margin-left': '10px'},
@@ -774,9 +810,9 @@ def accordion_items_from_pipe(
774
810
 
775
811
  update_sync_button = dbc.Button(
776
812
  "Sync",
777
- id = {'type': 'update-sync-button', 'index': json.dumps(pipe.meta)},
813
+ id = {'type': 'update-sync-button', 'index': pipe_meta_str},
778
814
  )
779
- sync_success_div = html.Div(id={'type': 'sync-success-div', 'index': json.dumps(pipe.meta)})
815
+ sync_success_div = html.Div(id={'type': 'sync-success-div', 'index': pipe_meta_str})
780
816
  items_bodies['sync-data'] = html.Div([
781
817
  sync_editor,
782
818
  html.Br(),