meerschaum 2.1.0rc2__py3-none-any.whl → 2.1.1rc1__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 (31) hide show
  1. meerschaum/actions/bootstrap.py +1 -2
  2. meerschaum/actions/delete.py +15 -1
  3. meerschaum/actions/sync.py +4 -4
  4. meerschaum/api/routes/_pipes.py +7 -11
  5. meerschaum/config/__init__.py +0 -2
  6. meerschaum/config/_default.py +3 -0
  7. meerschaum/config/_version.py +1 -1
  8. meerschaum/config/static/__init__.py +4 -0
  9. meerschaum/connectors/sql/SQLConnector.py +43 -3
  10. meerschaum/connectors/sql/_cli.py +27 -3
  11. meerschaum/connectors/sql/_instance.py +164 -0
  12. meerschaum/connectors/sql/_pipes.py +344 -304
  13. meerschaum/connectors/sql/_sql.py +52 -14
  14. meerschaum/connectors/sql/tables/__init__.py +65 -13
  15. meerschaum/connectors/sql/tables/pipes.py +9 -0
  16. meerschaum/core/Pipe/__init__.py +1 -1
  17. meerschaum/core/Pipe/_data.py +3 -4
  18. meerschaum/core/Pipe/_delete.py +12 -2
  19. meerschaum/core/Pipe/_sync.py +2 -5
  20. meerschaum/utils/dataframe.py +20 -4
  21. meerschaum/utils/dtypes/__init__.py +15 -1
  22. meerschaum/utils/dtypes/sql.py +1 -0
  23. meerschaum/utils/sql.py +485 -64
  24. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/METADATA +1 -1
  25. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/RECORD +31 -29
  26. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/LICENSE +0 -0
  27. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/NOTICE +0 -0
  28. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/WHEEL +0 -0
  29. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/entry_points.txt +0 -0
  30. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/top_level.txt +0 -0
  31. {meerschaum-2.1.0rc2.dist-info → meerschaum-2.1.1rc1.dist-info}/zip-safe +0 -0
@@ -15,7 +15,7 @@ def bootstrap(
15
15
  **kw: Any
16
16
  ) -> SuccessTuple:
17
17
  """
18
- Bootstrap an element (pipes, connectors, config).
18
+ Launch an interactive wizard to bootstrap pipes or connectors.
19
19
 
20
20
  Example:
21
21
  `bootstrap pipes`
@@ -24,7 +24,6 @@ def bootstrap(
24
24
  from meerschaum.actions import choose_subaction
25
25
  options = {
26
26
  'pipes' : _bootstrap_pipes,
27
- 'config' : _bootstrap_config,
28
27
  'connectors' : _bootstrap_connectors,
29
28
  }
30
29
  return choose_subaction(action, options, **kw)
@@ -64,6 +64,7 @@ def _complete_delete(
64
64
  from meerschaum._internal.shell import default_action_completer
65
65
  return default_action_completer(action=(['delete'] + action), **kw)
66
66
 
67
+
67
68
  def _delete_pipes(
68
69
  debug: bool = False,
69
70
  yes: bool = False,
@@ -79,13 +80,26 @@ def _delete_pipes(
79
80
  from meerschaum.utils.prompt import yes_no
80
81
  from meerschaum.utils.formatting import pprint, highlight_pipes
81
82
  from meerschaum.utils.warnings import warn
83
+ from meerschaum.actions import actions
82
84
  pipes = get_pipes(as_list=True, debug=debug, **kw)
83
85
  if len(pipes) == 0:
84
86
  return False, "No pipes to delete."
87
+
88
+ _ = kw.pop('action', None)
89
+ _ = actions['drop'](
90
+ ['pipes'],
91
+ yes = yes,
92
+ force = force,
93
+ noask = noask,
94
+ debug = debug,
95
+ **kw
96
+ )
97
+
85
98
  question = "Are you sure you want to delete these pipes? This can't be undone!\n"
86
99
  for p in pipes:
87
100
  question += f" - {p}" + "\n"
88
101
  question = highlight_pipes(question)
102
+
89
103
  answer = force
90
104
  if force:
91
105
  answer = True
@@ -98,7 +112,7 @@ def _delete_pipes(
98
112
  success_dict = {}
99
113
 
100
114
  for p in pipes:
101
- success_tuple = p.delete(debug=debug)
115
+ success_tuple = p.delete(drop=False, debug=debug)
102
116
  success_dict[p] = success_tuple[1]
103
117
  if success_tuple[0]:
104
118
  successes += 1
@@ -164,7 +164,7 @@ def _pipes_lap(
164
164
  remaining_count -= 1
165
165
  pipes_queue.task_done()
166
166
 
167
- sync_function_source = dill.source.getsource(_wrap_sync_pipe)
167
+ sync_function_source = dill.source.getsource(_wrap_pipe)
168
168
  fence_begin, fence_end = '<MRSM_RESULT>', '</MRSM_RESULT>'
169
169
 
170
170
  def sync_pipe(p):
@@ -173,7 +173,7 @@ def _pipes_lap(
173
173
  """
174
174
  ### If no timeout is specified, handle syncing in the current thread.
175
175
  if timeout_seconds is None:
176
- return _wrap_sync_pipe(p, **all_kw)
176
+ return _wrap_pipe(p, **all_kw)
177
177
  _success_tuple = False, "Nothing returned."
178
178
  def write_line(line):
179
179
  nonlocal _success_tuple
@@ -209,7 +209,7 @@ def _pipes_lap(
209
209
  + f"""print(
210
210
  '{fence_begin}'
211
211
  + json.dumps(
212
- _wrap_sync_pipe(
212
+ _wrap_pipe(
213
213
  pipe,
214
214
  **json.loads({json.dumps(json.dumps(all_kw))})
215
215
  )
@@ -384,7 +384,7 @@ def _sync_pipes(
384
384
  return (len(success_pipes) > 0 if success_pipes is not None else False), msg
385
385
 
386
386
 
387
- def _wrap_sync_pipe(
387
+ def _wrap_pipe(
388
388
  pipe,
389
389
  unblock: bool = False,
390
390
  force: bool = False,
@@ -87,7 +87,7 @@ def delete_pipe(
87
87
  ),
88
88
  ):
89
89
  """
90
- Delete a Pipe, dropping its table.
90
+ Delete a Pipe (without dropping its table).
91
91
  """
92
92
  from meerschaum.config import get_config
93
93
  allow_actions = get_config('system', 'api', 'permissions', 'actions', 'non_admin')
@@ -99,19 +99,19 @@ def delete_pipe(
99
99
  " Under the keys `api:permissions:actions`, " +
100
100
  "you can toggle non-admin actions."
101
101
  )
102
- pipe_object = get_pipe(connector_keys, metric_key, location_key)
103
- if not is_pipe_registered(pipe_object, pipes(refresh=True)):
102
+ pipe = get_pipe(connector_keys, metric_key, location_key)
103
+ if not is_pipe_registered(pipe, pipes(refresh=True)):
104
104
  raise fastapi.HTTPException(
105
- status_code=409, detail=f"{pipe_object} is not registered."
105
+ status_code=409, detail=f"{pipe} is not registered."
106
106
  )
107
- results = get_api_connector().delete_pipe(pipe_object, debug=debug)
107
+ results = get_api_connector().delete_pipe(pipe, debug=debug)
108
108
  pipes(refresh=True)
109
109
 
110
110
  return results
111
111
 
112
112
 
113
113
  @app.delete(pipes_endpoint + '/{connector_keys}/{metric_key}/{location_key}/drop', tags=['Pipes'])
114
- def delete_pipe(
114
+ def drop_pipe(
115
115
  connector_keys: str,
116
116
  metric_key: str,
117
117
  location_key: str,
@@ -120,7 +120,7 @@ def delete_pipe(
120
120
  ),
121
121
  ):
122
122
  """
123
- Drropping a pipes' table (without deleting its registration).
123
+ Dropping a pipes' table (without deleting its registration).
124
124
  """
125
125
  from meerschaum.config import get_config
126
126
  allow_actions = get_config('system', 'api', 'permissions', 'actions', 'non_admin')
@@ -133,10 +133,6 @@ def delete_pipe(
133
133
  "you can toggle non-admin actions."
134
134
  )
135
135
  pipe_object = get_pipe(connector_keys, metric_key, location_key)
136
- if not is_pipe_registered(pipe_object, pipes(refresh=True)):
137
- raise fastapi.HTTPException(
138
- status_code=409, detail=f"{pipe_object} is not registered."
139
- )
140
136
  results = get_api_connector().drop_pipe(pipe_object, debug=debug)
141
137
  pipes(refresh=True)
142
138
  return results
@@ -24,8 +24,6 @@ from meerschaum.config._paths import (
24
24
  DEFAULT_CONFIG_DIR_PATH,
25
25
  )
26
26
  from meerschaum.config._patch import (
27
- # permanent_patch_config,
28
- # patch_config,
29
27
  apply_patch_to_config,
30
28
  )
31
29
  __all__ = ('get_plugin_config', 'write_plugin_config', 'get_config', 'write_config', 'set_config',)
@@ -59,6 +59,9 @@ default_system_config = {
59
59
  'pandas': 'pandas',
60
60
  },
61
61
  'sql': {
62
+ 'instance': {
63
+ 'stale_temporary_tables_minutes': 1440,
64
+ },
62
65
  'chunksize': 100_000,
63
66
  'poolclass': 'sqlalchemy.pool.QueuePool',
64
67
  'create_engine': {
@@ -2,4 +2,4 @@
2
2
  Specify the Meerschaum release version.
3
3
  """
4
4
 
5
- __version__ = "2.1.0rc2"
5
+ __version__ = "2.1.1rc1"
@@ -40,6 +40,10 @@ STATIC_CONFIG: Dict[str, Any] = {
40
40
  },
41
41
  'webterm_job_name': '_webterm',
42
42
  },
43
+ 'sql': {
44
+ 'internal_schema': '_mrsm_internal',
45
+ 'instance_schema': 'mrsm',
46
+ },
43
47
  'environment': {
44
48
  'config': 'MRSM_CONFIG',
45
49
  'patch': 'MRSM_PATCH',
@@ -7,6 +7,7 @@ Interface with SQL servers using sqlalchemy.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+ import meerschaum as mrsm
10
11
  from meerschaum.utils.typing import Optional, Any, Union
11
12
 
12
13
  from meerschaum.connectors import Connector
@@ -28,7 +29,7 @@ class SQLConnector(Connector):
28
29
  from ._sql import read, value, exec, execute, to_sql, exec_queries
29
30
  from meerschaum.utils.sql import test_connection
30
31
  from ._fetch import fetch, get_pipe_metadef
31
- from ._cli import cli
32
+ from ._cli import cli, _cli_exit
32
33
  from ._pipes import (
33
34
  fetch_pipes_keys,
34
35
  create_indices,
@@ -78,6 +79,13 @@ class SQLConnector(Connector):
78
79
  get_user_attributes,
79
80
  )
80
81
  from ._uri import from_uri, parse_uri
82
+ from ._instance import (
83
+ _get_temporary_tables_pipe,
84
+ _log_temporary_tables_creation,
85
+ _drop_temporary_table,
86
+ _drop_temporary_tables,
87
+ _drop_old_temporary_tables,
88
+ )
81
89
 
82
90
  def __init__(
83
91
  self,
@@ -259,14 +267,46 @@ class SQLConnector(Connector):
259
267
  return ':memory:' not in self.URI
260
268
  return True
261
269
 
270
+
262
271
  @property
263
272
  def metadata(self):
273
+ """
274
+ Return the metadata bound to this configured schema.
275
+ """
264
276
  from meerschaum.utils.packages import attempt_import
265
277
  sqlalchemy = attempt_import('sqlalchemy')
266
278
  if '_metadata' not in self.__dict__:
267
279
  self._metadata = sqlalchemy.MetaData(schema=self.schema)
268
280
  return self._metadata
269
281
 
282
+
283
+ @property
284
+ def instance_schema(self):
285
+ """
286
+ Return the schema name for Meerschaum tables.
287
+ """
288
+ return self.schema
289
+
290
+
291
+ @property
292
+ def internal_schema(self):
293
+ """
294
+ Return the schema name for internal tables.
295
+ """
296
+ from meerschaum.config.static import STATIC_CONFIG
297
+ from meerschaum.utils.packages import attempt_import
298
+ from meerschaum.utils.sql import NO_SCHEMA_FLAVORS
299
+ schema_name = self.__dict__.get('internal_schema', None) or (
300
+ STATIC_CONFIG['sql']['internal_schema']
301
+ if self.flavor not in NO_SCHEMA_FLAVORS
302
+ else self.schema
303
+ )
304
+
305
+ if '_internal_schema' not in self.__dict__:
306
+ self._internal_schema = schema_name
307
+ return self._internal_schema
308
+
309
+
270
310
  @property
271
311
  def db(self) -> Optional[databases.Database]:
272
312
  from meerschaum.utils.packages import attempt_import
@@ -307,8 +347,8 @@ class SQLConnector(Connector):
307
347
  if 'schema' in self.__dict__:
308
348
  return self.__dict__['schema']
309
349
 
310
- uri = self.__dict__.get('uri', self.URI)
311
- _schema = self.parse_uri(uri).get('schema', None)
350
+ sqlalchemy = mrsm.attempt_import('sqlalchemy')
351
+ _schema = sqlalchemy.inspect(self.engine).default_schema_name
312
352
  self.__dict__['schema'] = _schema
313
353
  return _schema
314
354
 
@@ -7,6 +7,9 @@ Launch into a CLI environment to interact with the SQL Connector
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+ import os
11
+ import json
12
+ import copy
10
13
  ### NOTE: This import adds `Iterable` to collections, which is needed by some CLIs.
11
14
  from meerschaum.utils.typing import SuccessTuple
12
15
 
@@ -27,9 +30,32 @@ cli_deps = {
27
30
  'mycli': ['cryptography'],
28
31
  }
29
32
 
33
+
30
34
  def cli(
31
35
  self,
32
- debug : bool = False
36
+ debug: bool = False,
37
+ ) -> SuccessTuple:
38
+ """
39
+ Launch a subprocess for an interactive CLI.
40
+ """
41
+ from meerschaum.utils.venv import venv_exec
42
+ env = copy.deepcopy(dict(os.environ))
43
+ env[f'MRSM_SQL_{self.label.upper()}'] = json.dumps(self.meta)
44
+ cli_code = (
45
+ "import meerschaum as mrsm\n"
46
+ f"conn = mrsm.get_connector('sql:{self.label}')\n"
47
+ f"conn._cli_exit()"
48
+ )
49
+ try:
50
+ _ = venv_exec(cli_code, venv=None, debug=debug, capture_output=False)
51
+ except Exception as e:
52
+ return False, f"[{self}] Failed to start CLI:\n{e}"
53
+ return True, "Success"
54
+
55
+
56
+ def _cli_exit(
57
+ self,
58
+ debug: bool = False
33
59
  ) -> SuccessTuple:
34
60
  """Launch an interactive CLI for the SQLConnector's flavor."""
35
61
  from meerschaum.utils.packages import venv_exec, attempt_import
@@ -85,8 +111,6 @@ def cli(
85
111
  + "finally:\n"
86
112
  + " ms_object.shutdown()"
87
113
  )
88
- elif self.flavor == 'duckdb':
89
- launch_cli = ()
90
114
 
91
115
  try:
92
116
  if debug:
@@ -0,0 +1,164 @@
1
+ #! /usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # vim:fenc=utf-8
4
+
5
+ """
6
+ Define utilities for instance connectors.
7
+ """
8
+
9
+ import time
10
+ from datetime import datetime, timezone, timedelta
11
+ import meerschaum as mrsm
12
+ from meerschaum.utils.typing import Dict, SuccessTuple, Optional, Union, List
13
+ from meerschaum.utils.warnings import warn
14
+
15
+ def _get_temporary_tables_pipe(self) -> mrsm.Pipe:
16
+ """
17
+ Return a pipe for logging temporary tables.
18
+ """
19
+ return mrsm.Pipe(
20
+ "temporary", "tables",
21
+ target = "mrsm_temp_tables",
22
+ temporary = True,
23
+ instance = self,
24
+ columns = {
25
+ 'datetime': 'date_created',
26
+ 'table': 'table',
27
+ 'ready_to_drop': 'ready_to_drop',
28
+ },
29
+ dtypes = {
30
+ 'ready_to_drop': 'datetime',
31
+ },
32
+ parameters = {
33
+ 'schema': self.instance_schema,
34
+ },
35
+ )
36
+
37
+
38
+ def _log_temporary_tables_creation(
39
+ self,
40
+ tables: Union[str, List[str]],
41
+ ready_to_drop: bool = False,
42
+ debug: bool = False,
43
+ ) -> SuccessTuple:
44
+ """
45
+ Log a temporary table's creation for later deletion.
46
+ """
47
+ if isinstance(tables, str):
48
+ tables = [tables]
49
+ temporary_tables_pipe = self._get_temporary_tables_pipe()
50
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
51
+ return temporary_tables_pipe.sync(
52
+ [
53
+ {
54
+ 'date_created': now,
55
+ 'table': table,
56
+ 'ready_to_drop': (now if ready_to_drop else None),
57
+ }
58
+ for table in tables
59
+ ],
60
+ check_existing = False,
61
+ debug = debug,
62
+ )
63
+
64
+
65
+ def _drop_temporary_table(
66
+ self,
67
+ table: str,
68
+ debug: bool = False,
69
+ ) -> SuccessTuple:
70
+ """
71
+ Drop a temporary table and clear it from the internal table.
72
+ """
73
+ from meerschaum.utils.sql import sql_item_name, table_exists
74
+ temporary_tables_pipe = self._get_temporary_tables_pipe()
75
+ if table_exists(table, self, self.internal_schema, debug=debug):
76
+ return True, "Success"
77
+ drop_query = f"DROP TABLE IF EXISTS " + sql_item_name(
78
+ table, self.flavor, schema=self.internal_schema
79
+ )
80
+ drop_success = self.exec(drop_query, silent=True, debug=debug) is not None
81
+ drop_msg = "Success" if drop_success else f"Failed to drop temporary table '{table}'."
82
+ return drop_success, drop_msg
83
+
84
+
85
+ def _drop_temporary_tables(self, debug: bool = False) -> SuccessTuple:
86
+ """
87
+ Drop all tables in the internal schema that are marked as ready to be dropped.
88
+ """
89
+ from meerschaum.utils.sql import sql_item_name, table_exists
90
+ from meerschaum.utils.misc import items_str
91
+
92
+ temporary_tables_pipe = self._get_temporary_tables_pipe()
93
+ df = temporary_tables_pipe.get_data(['table'], params={'ready_to_drop': '_None'}, debug=debug)
94
+ if df is None:
95
+ return True, "Success"
96
+
97
+ dropped_tables = []
98
+ failed_tables = []
99
+ for table in df['table']:
100
+ drop_success, drop_msg = self._drop_temporary_table(table, debug=debug)
101
+ if not drop_success:
102
+ failed_tables.append(table)
103
+ continue
104
+ dropped_tables.append(table)
105
+
106
+ if dropped_tables:
107
+ temporary_tables_pipe.clear(params={'table': dropped_tables}, debug=debug)
108
+
109
+ success = len(failed_tables) == 0
110
+ msg = (
111
+ "Success"
112
+ if success
113
+ else (
114
+ "Failed to drop stale temporary tables "
115
+ + f"{items_str(failed_tables)}."
116
+ )
117
+ )
118
+ return success, msg
119
+
120
+
121
+ def _drop_old_temporary_tables(
122
+ self,
123
+ refresh: bool = True,
124
+ debug: bool = False,
125
+ ) -> SuccessTuple:
126
+ """
127
+ Drop temporary tables older than the configured interval (24 hours by default).
128
+ """
129
+ from meerschaum.config import get_config
130
+ last_check = getattr(self, '_stale_temporary_tables_check_timestamp', 0)
131
+ now_ts = time.perf_counter()
132
+ if refresh or not last_check or (now_ts - last_check) > 60:
133
+ self._stale_temporary_tables_check_timestamp = now_ts
134
+ return self._drop_temporary_tables(debug=debug)
135
+
136
+ stale_temporary_tables_minutes = get_config(
137
+ 'system', 'connectors', 'sql', 'instance', 'stale_temporary_tables_minutes'
138
+ )
139
+ temporary_tables_pipe = self._get_temporary_tables_pipe()
140
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
141
+ end = now - timedelta(minutes=stale_temporary_tables_minutes)
142
+ df = temporary_tables_pipe.get_data(end=end, debug=debug)
143
+ if df is None:
144
+ return True, "Success"
145
+
146
+ ### Insert new records with the current time (skipping updates to avoid recursion).
147
+ docs = [
148
+ {
149
+ 'date_created': now,
150
+ 'table': doc['table'],
151
+ 'ready_to_drop': now,
152
+ }
153
+ for doc in df.to_dict(orient='records')
154
+ ]
155
+ if docs:
156
+ update_temporary_success, update_temporary_msg = temporary_tables_pipe.sync(
157
+ docs,
158
+ check_existing = False,
159
+ debug = debug,
160
+ )
161
+ if not update_temporary_success:
162
+ warn(update_temporary_msg)
163
+
164
+ return self._drop_temporary_tables(debug=debug)