meerschaum 2.1.6__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meerschaum/__main__.py +1 -1
- meerschaum/_internal/arguments/_parser.py +3 -0
- meerschaum/_internal/entry.py +3 -2
- meerschaum/_internal/shell/Shell.py +1 -6
- meerschaum/actions/api.py +1 -1
- meerschaum/actions/install.py +7 -3
- meerschaum/actions/show.py +128 -42
- meerschaum/actions/sync.py +7 -3
- meerschaum/api/__init__.py +24 -14
- meerschaum/api/_oauth2.py +4 -4
- meerschaum/api/dash/callbacks/dashboard.py +93 -23
- meerschaum/api/dash/callbacks/jobs.py +55 -3
- meerschaum/api/dash/jobs.py +34 -8
- meerschaum/api/dash/keys.py +1 -1
- meerschaum/api/dash/pages/dashboard.py +14 -4
- meerschaum/api/dash/pipes.py +137 -26
- meerschaum/api/dash/plugins.py +25 -9
- meerschaum/api/resources/static/js/xterm.js +1 -1
- meerschaum/api/resources/templates/termpage.html +3 -0
- meerschaum/api/routes/_login.py +5 -4
- meerschaum/api/routes/_plugins.py +6 -3
- meerschaum/config/_dash.py +11 -0
- meerschaum/config/_default.py +3 -1
- meerschaum/config/_jobs.py +13 -4
- meerschaum/config/_paths.py +2 -0
- meerschaum/config/_shell.py +0 -1
- meerschaum/config/_sync.py +2 -3
- meerschaum/config/_version.py +1 -1
- meerschaum/config/stack/__init__.py +6 -7
- meerschaum/config/stack/grafana/__init__.py +1 -1
- meerschaum/config/static/__init__.py +4 -1
- meerschaum/connectors/__init__.py +2 -0
- meerschaum/connectors/api/_plugins.py +2 -1
- meerschaum/connectors/sql/SQLConnector.py +4 -2
- meerschaum/connectors/sql/_create_engine.py +9 -9
- meerschaum/connectors/sql/_fetch.py +8 -11
- meerschaum/connectors/sql/_instance.py +3 -1
- meerschaum/connectors/sql/_pipes.py +61 -39
- meerschaum/connectors/sql/_plugins.py +0 -2
- meerschaum/connectors/sql/_sql.py +7 -9
- meerschaum/core/Pipe/_dtypes.py +2 -1
- meerschaum/core/Pipe/_sync.py +26 -13
- meerschaum/core/User/_User.py +158 -16
- meerschaum/core/User/__init__.py +1 -1
- meerschaum/plugins/_Plugin.py +12 -3
- meerschaum/plugins/__init__.py +23 -1
- meerschaum/utils/daemon/Daemon.py +89 -36
- meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
- meerschaum/utils/daemon/RotatingFile.py +130 -14
- meerschaum/utils/daemon/__init__.py +3 -0
- meerschaum/utils/dataframe.py +183 -8
- meerschaum/utils/dtypes/__init__.py +9 -5
- meerschaum/utils/formatting/_pipes.py +44 -10
- meerschaum/utils/misc.py +34 -2
- meerschaum/utils/packages/__init__.py +25 -8
- meerschaum/utils/packages/_packages.py +18 -20
- meerschaum/utils/process.py +13 -10
- meerschaum/utils/schedule.py +276 -30
- meerschaum/utils/threading.py +1 -0
- meerschaum/utils/typing.py +1 -1
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/RECORD +68 -66
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
- {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
meerschaum/api/routes/_login.py
CHANGED
@@ -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
|
42
|
+
from meerschaum.core.User._User import verify_password
|
43
43
|
user = User(username, password)
|
44
|
-
correct_password = no_auth or
|
45
|
-
password,
|
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 =
|
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')
|
meerschaum/config/_default.py
CHANGED
@@ -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
|
#####################################################################
|
meerschaum/config/_jobs.py
CHANGED
@@ -9,13 +9,22 @@ Default configuration for jobs.
|
|
9
9
|
default_jobs_config = {
|
10
10
|
'timeout_seconds': 8,
|
11
11
|
'check_timeout_interval_seconds': 0.1,
|
12
|
-
'
|
12
|
+
'terminal': {
|
13
|
+
'lines': 40,
|
14
|
+
'columns': 70,
|
15
|
+
},
|
16
|
+
'logs': {
|
17
|
+
'timestamps': {
|
18
|
+
'enabled': True,
|
19
|
+
'format': '%Y-%m-%d %H:%M',
|
20
|
+
'follow_format': '%H:%M',
|
21
|
+
},
|
13
22
|
'num_files_to_keep': 5,
|
14
23
|
'max_file_size': 100_000,
|
15
24
|
'lines_to_show': 30,
|
16
|
-
'refresh_files_seconds': 5
|
17
|
-
'min_buffer_len':
|
18
|
-
'colors'
|
25
|
+
'refresh_files_seconds': 5,
|
26
|
+
'min_buffer_len': 5,
|
27
|
+
'colors': [
|
19
28
|
'cyan',
|
20
29
|
'magenta',
|
21
30
|
'orange3',
|
meerschaum/config/_paths.py
CHANGED
@@ -129,6 +129,7 @@ paths = {
|
|
129
129
|
|
130
130
|
'PLUGINS_RESOURCES_PATH' : ('{INTERNAL_RESOURCES_PATH}', 'plugins'),
|
131
131
|
'PLUGINS_INTERNAL_LOCK_PATH' : ('{INTERNAL_RESOURCES_PATH}', 'plugins.lock'),
|
132
|
+
'PLUGINS_PACKAGES_INTERNAL_PATH' : ('{INTERNAL_RESOURCES_PATH}', 'packaged_plugins'),
|
132
133
|
'PLUGINS_ARCHIVES_RESOURCES_PATH': ('{PLUGINS_RESOURCES_PATH}', '.archives'),
|
133
134
|
'PLUGINS_TEMP_RESOURCES_PATH' : ('{PLUGINS_RESOURCES_PATH}', '.tmp'),
|
134
135
|
'PLUGINS_INIT_PATH' : ('{PLUGINS_RESOURCES_PATH}', '__init__.py'),
|
@@ -153,6 +154,7 @@ paths = {
|
|
153
154
|
|
154
155
|
'DAEMON_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'jobs'),
|
155
156
|
'LOGS_RESOURCES_PATH' : ('{ROOT_DIR_PATH}', 'logs'),
|
157
|
+
'DAEMON_ERROR_LOG_PATH' : ('{ROOT_DIR_PATH}', 'daemon_errors.log'),
|
156
158
|
}
|
157
159
|
|
158
160
|
def set_root(root: Union[Path, str]):
|
meerschaum/config/_shell.py
CHANGED
meerschaum/config/_sync.py
CHANGED
@@ -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
|
-
|
meerschaum/config/_version.py
CHANGED
@@ -33,7 +33,7 @@ api_host = "api"
|
|
33
33
|
|
34
34
|
env_dict = {
|
35
35
|
'COMPOSE_PROJECT_NAME' : 'mrsm',
|
36
|
-
'TIMESCALEDB_VERSION' : 'latest-
|
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}',
|
@@ -97,7 +97,6 @@ compose_header = """
|
|
97
97
|
|
98
98
|
|
99
99
|
default_docker_compose_config = {
|
100
|
-
'version': '3.9',
|
101
100
|
'services': {
|
102
101
|
'db': {
|
103
102
|
'environment': {
|
@@ -233,11 +232,11 @@ NECESSARY_FILES = [STACK_COMPOSE_PATH, GRAFANA_DATASOURCE_PATH, GRAFANA_DASHBOAR
|
|
233
232
|
def get_necessary_files():
|
234
233
|
from meerschaum.config import get_config
|
235
234
|
return {
|
236
|
-
STACK_COMPOSE_PATH
|
235
|
+
STACK_COMPOSE_PATH: (
|
237
236
|
get_config('stack', STACK_COMPOSE_FILENAME, substitute=True), compose_header
|
238
237
|
),
|
239
|
-
GRAFANA_DATASOURCE_PATH
|
240
|
-
GRAFANA_DASHBOARD_PATH
|
238
|
+
GRAFANA_DATASOURCE_PATH: get_config('stack', 'grafana', 'datasource', substitute=True),
|
239
|
+
GRAFANA_DASHBOARD_PATH: get_config('stack', 'grafana', 'dashboard', substitute=True),
|
241
240
|
}
|
242
241
|
|
243
242
|
|
@@ -251,8 +250,8 @@ def write_stack(
|
|
251
250
|
return sync_files(['stack'])
|
252
251
|
|
253
252
|
def edit_stack(
|
254
|
-
action
|
255
|
-
debug
|
253
|
+
action: Optional[List[str]] = None,
|
254
|
+
debug: bool = False,
|
256
255
|
**kw
|
257
256
|
):
|
258
257
|
"""Open docker-compose.yaml or .env for editing."""
|
@@ -60,6 +60,7 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
60
60
|
'gid': 'MRSM_GID',
|
61
61
|
'noask': 'MRSM_NOASK',
|
62
62
|
'id': 'MRSM_SERVER_ID',
|
63
|
+
'daemon_id': 'MRSM_DAEMON_ID',
|
63
64
|
'uri_regex': r'MRSM_([a-zA-Z0-9]*)_(\d*[a-zA-Z][a-zA-Z0-9-_+]*$)',
|
64
65
|
'prefix': 'MRSM_',
|
65
66
|
},
|
@@ -103,11 +104,13 @@ STATIC_CONFIG: Dict[str, Any] = {
|
|
103
104
|
},
|
104
105
|
'users': {
|
105
106
|
'password_hash': {
|
107
|
+
'algorithm_name': 'sha256',
|
108
|
+
'salt_bytes': 16,
|
106
109
|
'schemes': [
|
107
110
|
'pbkdf2_sha256',
|
108
111
|
],
|
109
112
|
'default': 'pbkdf2_sha256',
|
110
|
-
'pbkdf2_sha256__default_rounds':
|
113
|
+
'pbkdf2_sha256__default_rounds': 3_000_000,
|
111
114
|
},
|
112
115
|
'min_username_length': 1,
|
113
116
|
'max_username_length': 26,
|
@@ -317,6 +317,8 @@ def load_plugin_connectors():
|
|
317
317
|
from meerschaum.plugins import get_plugins, import_plugins
|
318
318
|
to_import = []
|
319
319
|
for plugin in get_plugins():
|
320
|
+
if plugin is None:
|
321
|
+
continue
|
320
322
|
with open(plugin.__file__, encoding='utf-8') as f:
|
321
323
|
text = f.read()
|
322
324
|
if 'make_connector' in text:
|
@@ -49,6 +49,7 @@ def register_plugin(
|
|
49
49
|
def install_plugin(
|
50
50
|
self,
|
51
51
|
name: str,
|
52
|
+
skip_deps: bool = False,
|
52
53
|
force: bool = False,
|
53
54
|
debug: bool = False
|
54
55
|
) -> SuccessTuple:
|
@@ -78,7 +79,7 @@ def install_plugin(
|
|
78
79
|
success, msg = False, fail_msg
|
79
80
|
return success, msg
|
80
81
|
plugin = Plugin(name, archive_path=archive_path, repo_connector=self)
|
81
|
-
return plugin.install(force=force, debug=debug)
|
82
|
+
return plugin.install(skip_deps=skip_deps, force=force, debug=debug)
|
82
83
|
|
83
84
|
def get_plugins(
|
84
85
|
self,
|
@@ -128,8 +128,10 @@ class SQLConnector(Connector):
|
|
128
128
|
"""
|
129
129
|
if 'uri' in kw:
|
130
130
|
uri = kw['uri']
|
131
|
-
if uri.startswith('postgres
|
132
|
-
uri = uri.replace('postgres
|
131
|
+
if uri.startswith('postgres') and not uri.startswith('postgresql'):
|
132
|
+
uri = uri.replace('postgres', 'postgresql', 1)
|
133
|
+
if uri.startswith('postgresql') and not uri.startswith('postgresql+'):
|
134
|
+
uri = uri.replace('postgresql://', 'postgresql+psycopg', 1)
|
133
135
|
if uri.startswith('timescaledb://'):
|
134
136
|
uri = uri.replace('timescaledb://', 'postgresql://', 1)
|
135
137
|
flavor = 'timescaledb'
|
@@ -28,7 +28,7 @@ default_create_engine_args = {
|
|
28
28
|
}
|
29
29
|
flavor_configs = {
|
30
30
|
'timescaledb' : {
|
31
|
-
'engine' : 'postgresql',
|
31
|
+
'engine' : 'postgresql+psycopg',
|
32
32
|
'create_engine' : default_create_engine_args,
|
33
33
|
'omit_create_engine': {'method',},
|
34
34
|
'to_sql' : {},
|
@@ -38,7 +38,7 @@ flavor_configs = {
|
|
38
38
|
},
|
39
39
|
},
|
40
40
|
'postgresql' : {
|
41
|
-
'engine' : 'postgresql',
|
41
|
+
'engine' : 'postgresql+psycopg',
|
42
42
|
'create_engine' : default_create_engine_args,
|
43
43
|
'omit_create_engine': {'method',},
|
44
44
|
'to_sql' : {},
|
@@ -48,7 +48,7 @@ flavor_configs = {
|
|
48
48
|
},
|
49
49
|
},
|
50
50
|
'citus' : {
|
51
|
-
'engine' : 'postgresql',
|
51
|
+
'engine' : 'postgresql+psycopg',
|
52
52
|
'create_engine' : default_create_engine_args,
|
53
53
|
'omit_create_engine': {'method',},
|
54
54
|
'to_sql' : {},
|
@@ -154,10 +154,10 @@ install_flavor_drivers = {
|
|
154
154
|
'duckdb': ['duckdb', 'duckdb_engine'],
|
155
155
|
'mysql': ['pymysql'],
|
156
156
|
'mariadb': ['pymysql'],
|
157
|
-
'timescaledb': ['
|
158
|
-
'postgresql': ['
|
159
|
-
'citus': ['
|
160
|
-
'cockroachdb': ['
|
157
|
+
'timescaledb': ['psycopg'],
|
158
|
+
'postgresql': ['psycopg'],
|
159
|
+
'citus': ['psycopg'],
|
160
|
+
'cockroachdb': ['psycopg', 'sqlalchemy_cockroachdb', 'sqlalchemy_cockroachdb.psycopg'],
|
161
161
|
'mssql': ['pyodbc'],
|
162
162
|
'oracle': ['cx_Oracle'],
|
163
163
|
}
|
@@ -165,7 +165,7 @@ require_patching_flavors = {'cockroachdb': [('sqlalchemy-cockroachdb', 'sqlalche
|
|
165
165
|
|
166
166
|
flavor_dialects = {
|
167
167
|
'cockroachdb': (
|
168
|
-
'cockroachdb', 'sqlalchemy_cockroachdb.
|
168
|
+
'cockroachdb', 'sqlalchemy_cockroachdb.psycopg', 'CockroachDBDialect_psycopg'
|
169
169
|
),
|
170
170
|
'duckdb': ('duckdb', 'duckdb_engine', 'Dialect'),
|
171
171
|
}
|
@@ -242,7 +242,7 @@ def create_engine(
|
|
242
242
|
|
243
243
|
### Sometimes the timescaledb:// flavor can slip in.
|
244
244
|
if _uri and self.flavor in ('timescaledb',) and self.flavor in _uri:
|
245
|
-
engine_str = engine_str.replace(f'{self.flavor}
|
245
|
+
engine_str = engine_str.replace(f'{self.flavor}', 'postgresql', 1)
|
246
246
|
|
247
247
|
if debug:
|
248
248
|
dprint(
|
@@ -174,9 +174,6 @@ def get_pipe_metadef(
|
|
174
174
|
)
|
175
175
|
|
176
176
|
|
177
|
-
if 'order by' in definition.lower() and 'over' not in definition.lower():
|
178
|
-
error("Cannot fetch with an ORDER clause in the definition")
|
179
|
-
|
180
177
|
apply_backtrack = begin == '' and check_existing
|
181
178
|
backtrack_interval = pipe.get_backtrack_interval(check_existing=check_existing, debug=debug)
|
182
179
|
btm = (
|
@@ -308,9 +305,9 @@ def _simple_fetch_query(pipe, debug: bool=False, **kw) -> str:
|
|
308
305
|
def_name = 'definition'
|
309
306
|
definition = get_pipe_query(pipe)
|
310
307
|
return (
|
311
|
-
f"WITH {def_name} AS ({definition}) SELECT * FROM {def_name}"
|
308
|
+
f"WITH {def_name} AS (\n{definition}\n) SELECT * FROM {def_name}"
|
312
309
|
if pipe.connector.flavor not in ('mysql', 'mariadb')
|
313
|
-
else f"SELECT * FROM ({definition}) AS {def_name}"
|
310
|
+
else f"SELECT * FROM (\n{definition}\n) AS {def_name}"
|
314
311
|
)
|
315
312
|
|
316
313
|
def _join_fetch_query(
|
@@ -363,10 +360,10 @@ def _join_fetch_query(
|
|
363
360
|
)
|
364
361
|
+ f") AS {id_remote_name}, "
|
365
362
|
+ dateadd_str(
|
366
|
-
flavor=pipe.connector.flavor,
|
367
|
-
begin=_st,
|
368
|
-
datepart='minute',
|
369
|
-
number=pipe.parameters.get('fetch', {}).get('backtrack_minutes', 0)
|
363
|
+
flavor = pipe.connector.flavor,
|
364
|
+
begin = _st,
|
365
|
+
datepart = 'minute',
|
366
|
+
number = pipe.parameters.get('fetch', {}).get('backtrack_minutes', 0)
|
370
367
|
) + " AS " + dt_remote_name + "\nUNION ALL\n"
|
371
368
|
)
|
372
369
|
_sync_times_q = _sync_times_q[:(-1 * len('UNION ALL\n'))] + ")"
|
@@ -374,13 +371,13 @@ def _join_fetch_query(
|
|
374
371
|
definition = get_pipe_query(pipe)
|
375
372
|
query = (
|
376
373
|
f"""
|
377
|
-
WITH definition AS ({definition}){_sync_times_q}
|
374
|
+
WITH definition AS (\n{definition}\n){_sync_times_q}
|
378
375
|
SELECT definition.*
|
379
376
|
FROM definition"""
|
380
377
|
if pipe.connector.flavor not in ('mysql', 'mariadb')
|
381
378
|
else (
|
382
379
|
f"""
|
383
|
-
SELECT * FROM ({definition}) AS definition"""
|
380
|
+
SELECT * FROM (\n{definition}\n) AS definition"""
|
384
381
|
)
|
385
382
|
) + f"""
|
386
383
|
LEFT OUTER JOIN {sync_times_remote_name} AS st
|
@@ -155,7 +155,9 @@ def _drop_old_temporary_tables(
|
|
155
155
|
temp_tables_table = get_tables(mrsm_instance=self, create=False, debug=debug)['temp_tables']
|
156
156
|
last_check = getattr(self, '_stale_temporary_tables_check_timestamp', 0)
|
157
157
|
now_ts = time.perf_counter()
|
158
|
-
if
|
158
|
+
if not last_check:
|
159
|
+
self._stale_temporary_tables_check_timestamp = 0
|
160
|
+
if refresh or (now_ts - last_check) < 60:
|
159
161
|
self._stale_temporary_tables_check_timestamp = now_ts
|
160
162
|
return self._drop_temporary_tables(debug=debug)
|
161
163
|
|
@@ -752,7 +752,7 @@ def get_pipe_data(
|
|
752
752
|
debug = debug,
|
753
753
|
**kw
|
754
754
|
)
|
755
|
-
|
755
|
+
|
756
756
|
if is_dask:
|
757
757
|
index_col = pipe.columns.get('datetime', None)
|
758
758
|
kw['index_col'] = index_col
|
@@ -763,6 +763,7 @@ def get_pipe_data(
|
|
763
763
|
if typ == 'numeric' and col in dtypes
|
764
764
|
]
|
765
765
|
kw['coerce_float'] = kw.get('coerce_float', (len(numeric_columns) == 0))
|
766
|
+
|
766
767
|
df = self.read(
|
767
768
|
query,
|
768
769
|
dtype = dtypes,
|
@@ -1182,7 +1183,12 @@ def sync_pipe(
|
|
1182
1183
|
dprint("Fetched data:\n" + str(df))
|
1183
1184
|
|
1184
1185
|
if not isinstance(df, pd.DataFrame):
|
1185
|
-
df = pipe.enforce_dtypes(
|
1186
|
+
df = pipe.enforce_dtypes(
|
1187
|
+
df,
|
1188
|
+
chunksize = chunksize,
|
1189
|
+
safe_copy = kw.get('safe_copy', False),
|
1190
|
+
debug = debug,
|
1191
|
+
)
|
1186
1192
|
|
1187
1193
|
### if table does not exist, create it with indices
|
1188
1194
|
is_new = False
|
@@ -1226,6 +1232,7 @@ def sync_pipe(
|
|
1226
1232
|
upsert = pipe.parameters.get('upsert', False) and (self.flavor + '-upsert') in update_queries
|
1227
1233
|
if upsert:
|
1228
1234
|
check_existing = False
|
1235
|
+
kw['safe_copy'] = kw.get('safe_copy', False)
|
1229
1236
|
|
1230
1237
|
unseen_df, update_df, delta_df = (
|
1231
1238
|
pipe.filter_existing(
|
@@ -1472,43 +1479,11 @@ def sync_pipe_inplace(
|
|
1472
1479
|
from meerschaum.utils.misc import generate_password
|
1473
1480
|
from meerschaum.utils.debug import dprint
|
1474
1481
|
|
1475
|
-
sqlalchemy, sqlalchemy_orm = mrsm.attempt_import('sqlalchemy', 'sqlalchemy.orm')
|
1476
|
-
metadef = self.get_pipe_metadef(
|
1477
|
-
pipe,
|
1478
|
-
params = params,
|
1479
|
-
begin = begin,
|
1480
|
-
end = end,
|
1481
|
-
check_existing = check_existing,
|
1482
|
-
debug = debug,
|
1483
|
-
)
|
1484
|
-
pipe_name = sql_item_name(pipe.target, self.flavor, self.get_pipe_schema(pipe))
|
1485
|
-
upsert = pipe.parameters.get('upsert', False) and f'{self.flavor}-upsert' in update_queries
|
1486
|
-
internal_schema = self.internal_schema
|
1487
|
-
database = getattr(self, 'database', self.parse_uri(self.URI).get('database', None))
|
1488
|
-
|
1489
|
-
if not pipe.exists(debug=debug):
|
1490
|
-
create_pipe_query = get_create_table_query(
|
1491
|
-
metadef,
|
1492
|
-
pipe.target,
|
1493
|
-
self.flavor,
|
1494
|
-
schema = self.get_pipe_schema(pipe),
|
1495
|
-
)
|
1496
|
-
result = self.exec(create_pipe_query, debug=debug)
|
1497
|
-
if result is None:
|
1498
|
-
return False, f"Could not insert new data into {pipe} from its SQL query definition."
|
1499
|
-
if not self.create_indices(pipe, debug=debug):
|
1500
|
-
warn(f"Failed to create indices for {pipe}. Continuing...")
|
1501
|
-
|
1502
|
-
rowcount = pipe.get_rowcount(debug=debug)
|
1503
|
-
return True, f"Inserted {rowcount}, updated 0 rows."
|
1504
|
-
|
1505
|
-
session = sqlalchemy_orm.Session(self.engine)
|
1506
|
-
connectable = session if self.flavor != 'duckdb' else self
|
1507
|
-
|
1508
1482
|
transact_id = generate_password(3)
|
1509
1483
|
def get_temp_table_name(label: str) -> str:
|
1510
1484
|
return '-' + transact_id + '_' + label + '_' + pipe.target
|
1511
1485
|
|
1486
|
+
internal_schema = self.internal_schema
|
1512
1487
|
temp_table_roots = ['backtrack', 'new', 'delta', 'joined', 'unseen', 'update']
|
1513
1488
|
temp_tables = {
|
1514
1489
|
table_root: get_temp_table_name(table_root)
|
@@ -1522,6 +1497,17 @@ def sync_pipe_inplace(
|
|
1522
1497
|
)
|
1523
1498
|
for table_root, table_name_raw in temp_tables.items()
|
1524
1499
|
}
|
1500
|
+
metadef = self.get_pipe_metadef(
|
1501
|
+
pipe,
|
1502
|
+
params = params,
|
1503
|
+
begin = begin,
|
1504
|
+
end = end,
|
1505
|
+
check_existing = check_existing,
|
1506
|
+
debug = debug,
|
1507
|
+
)
|
1508
|
+
pipe_name = sql_item_name(pipe.target, self.flavor, self.get_pipe_schema(pipe))
|
1509
|
+
upsert = pipe.parameters.get('upsert', False) and f'{self.flavor}-upsert' in update_queries
|
1510
|
+
database = getattr(self, 'database', self.parse_uri(self.URI).get('database', None))
|
1525
1511
|
|
1526
1512
|
def clean_up_temp_tables(ready_to_drop: bool = False):
|
1527
1513
|
log_success, log_msg = self._log_temporary_tables_creation(
|
@@ -1535,6 +1521,36 @@ def sync_pipe_inplace(
|
|
1535
1521
|
)
|
1536
1522
|
if not log_success:
|
1537
1523
|
warn(log_msg)
|
1524
|
+
drop_stale_success, drop_stale_msg = self._drop_old_temporary_tables(
|
1525
|
+
refresh = False,
|
1526
|
+
debug = debug,
|
1527
|
+
)
|
1528
|
+
if not drop_stale_success:
|
1529
|
+
warn(drop_stale_msg)
|
1530
|
+
return drop_stale_success, drop_stale_msg
|
1531
|
+
|
1532
|
+
sqlalchemy, sqlalchemy_orm = mrsm.attempt_import('sqlalchemy', 'sqlalchemy.orm')
|
1533
|
+
if not pipe.exists(debug=debug):
|
1534
|
+
create_pipe_query = get_create_table_query(
|
1535
|
+
metadef,
|
1536
|
+
pipe.target,
|
1537
|
+
self.flavor,
|
1538
|
+
schema = self.get_pipe_schema(pipe),
|
1539
|
+
)
|
1540
|
+
result = self.exec(create_pipe_query, debug=debug)
|
1541
|
+
if result is None:
|
1542
|
+
_ = clean_up_temp_tables()
|
1543
|
+
return False, f"Could not insert new data into {pipe} from its SQL query definition."
|
1544
|
+
|
1545
|
+
if not self.create_indices(pipe, debug=debug):
|
1546
|
+
warn(f"Failed to create indices for {pipe}. Continuing...")
|
1547
|
+
|
1548
|
+
rowcount = pipe.get_rowcount(debug=debug)
|
1549
|
+
_ = clean_up_temp_tables()
|
1550
|
+
return True, f"Inserted {rowcount}, updated 0 rows."
|
1551
|
+
|
1552
|
+
session = sqlalchemy_orm.Session(self.engine)
|
1553
|
+
connectable = session if self.flavor != 'duckdb' else self
|
1538
1554
|
|
1539
1555
|
create_new_query = get_create_table_query(
|
1540
1556
|
metadef,
|
@@ -1902,10 +1918,6 @@ def sync_pipe_inplace(
|
|
1902
1918
|
)
|
1903
1919
|
_ = clean_up_temp_tables(ready_to_drop=True)
|
1904
1920
|
|
1905
|
-
drop_stale_success, drop_stale_msg = self._drop_old_temporary_tables(refresh=False, debug=debug)
|
1906
|
-
if not drop_stale_success:
|
1907
|
-
warn(drop_stale_msg)
|
1908
|
-
|
1909
1921
|
return True, msg
|
1910
1922
|
|
1911
1923
|
|
@@ -2366,6 +2378,16 @@ def get_pipe_columns_types(
|
|
2366
2378
|
"""
|
2367
2379
|
if not pipe.exists(debug=debug):
|
2368
2380
|
return {}
|
2381
|
+
|
2382
|
+
if self.flavor == 'duckdb':
|
2383
|
+
from meerschaum.utils.sql import get_table_cols_types
|
2384
|
+
return get_table_cols_types(
|
2385
|
+
pipe.target,
|
2386
|
+
self,
|
2387
|
+
flavor = self.flavor,
|
2388
|
+
schema = self.schema,
|
2389
|
+
)
|
2390
|
+
|
2369
2391
|
table_columns = {}
|
2370
2392
|
try:
|
2371
2393
|
pipe_table = self.get_pipe_table(pipe, debug=debug)
|
@@ -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(
|
@@ -943,17 +943,15 @@ def psql_insert_copy(
|
|
943
943
|
) for row in data_iter
|
944
944
|
)
|
945
945
|
|
946
|
+
table_name = sql_item_name(table.name, 'postgresql', table.schema)
|
947
|
+
columns = ', '.join(f'"{k}"' for k in keys)
|
948
|
+
sql = f"COPY {table_name} ({columns}) FROM STDIN WITH CSV NULL '\\N'"
|
949
|
+
|
946
950
|
dbapi_conn = conn.connection
|
947
951
|
with dbapi_conn.cursor() as cur:
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
s_buf.seek(0)
|
952
|
-
|
953
|
-
columns = ', '.join(f'"{k}"' for k in keys)
|
954
|
-
table_name = sql_item_name(table.name, 'postgresql', table.schema)
|
955
|
-
sql = f"COPY {table_name} ({columns}) FROM STDIN WITH CSV NULL '\\N'"
|
956
|
-
cur.copy_expert(sql=sql, file=s_buf)
|
952
|
+
with cur.copy(sql) as copy:
|
953
|
+
writer = csv.writer(copy)
|
954
|
+
writer.writerows(data_iter)
|
957
955
|
|
958
956
|
|
959
957
|
def format_sql_query_for_dask(query: str) -> 'sqlalchemy.sql.selectable.Select':
|
meerschaum/core/Pipe/_dtypes.py
CHANGED
@@ -14,6 +14,7 @@ def enforce_dtypes(
|
|
14
14
|
self,
|
15
15
|
df: 'pd.DataFrame',
|
16
16
|
chunksize: Optional[int] = -1,
|
17
|
+
safe_copy: bool = True,
|
17
18
|
debug: bool = False,
|
18
19
|
) -> 'pd.DataFrame':
|
19
20
|
"""
|
@@ -71,7 +72,7 @@ def enforce_dtypes(
|
|
71
72
|
)
|
72
73
|
return df
|
73
74
|
|
74
|
-
return _enforce_dtypes(df, pipe_dtypes, debug=debug)
|
75
|
+
return _enforce_dtypes(df, pipe_dtypes, safe_copy=safe_copy, debug=debug)
|
75
76
|
|
76
77
|
|
77
78
|
def infer_dtypes(self, persist: bool=False, debug: bool=False) -> Dict[str, Any]:
|