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.
Files changed (68) hide show
  1. meerschaum/__main__.py +1 -1
  2. meerschaum/_internal/arguments/_parser.py +3 -0
  3. meerschaum/_internal/entry.py +3 -2
  4. meerschaum/_internal/shell/Shell.py +1 -6
  5. meerschaum/actions/api.py +1 -1
  6. meerschaum/actions/install.py +7 -3
  7. meerschaum/actions/show.py +128 -42
  8. meerschaum/actions/sync.py +7 -3
  9. meerschaum/api/__init__.py +24 -14
  10. meerschaum/api/_oauth2.py +4 -4
  11. meerschaum/api/dash/callbacks/dashboard.py +93 -23
  12. meerschaum/api/dash/callbacks/jobs.py +55 -3
  13. meerschaum/api/dash/jobs.py +34 -8
  14. meerschaum/api/dash/keys.py +1 -1
  15. meerschaum/api/dash/pages/dashboard.py +14 -4
  16. meerschaum/api/dash/pipes.py +137 -26
  17. meerschaum/api/dash/plugins.py +25 -9
  18. meerschaum/api/resources/static/js/xterm.js +1 -1
  19. meerschaum/api/resources/templates/termpage.html +3 -0
  20. meerschaum/api/routes/_login.py +5 -4
  21. meerschaum/api/routes/_plugins.py +6 -3
  22. meerschaum/config/_dash.py +11 -0
  23. meerschaum/config/_default.py +3 -1
  24. meerschaum/config/_jobs.py +13 -4
  25. meerschaum/config/_paths.py +2 -0
  26. meerschaum/config/_shell.py +0 -1
  27. meerschaum/config/_sync.py +2 -3
  28. meerschaum/config/_version.py +1 -1
  29. meerschaum/config/stack/__init__.py +6 -7
  30. meerschaum/config/stack/grafana/__init__.py +1 -1
  31. meerschaum/config/static/__init__.py +4 -1
  32. meerschaum/connectors/__init__.py +2 -0
  33. meerschaum/connectors/api/_plugins.py +2 -1
  34. meerschaum/connectors/sql/SQLConnector.py +4 -2
  35. meerschaum/connectors/sql/_create_engine.py +9 -9
  36. meerschaum/connectors/sql/_fetch.py +8 -11
  37. meerschaum/connectors/sql/_instance.py +3 -1
  38. meerschaum/connectors/sql/_pipes.py +61 -39
  39. meerschaum/connectors/sql/_plugins.py +0 -2
  40. meerschaum/connectors/sql/_sql.py +7 -9
  41. meerschaum/core/Pipe/_dtypes.py +2 -1
  42. meerschaum/core/Pipe/_sync.py +26 -13
  43. meerschaum/core/User/_User.py +158 -16
  44. meerschaum/core/User/__init__.py +1 -1
  45. meerschaum/plugins/_Plugin.py +12 -3
  46. meerschaum/plugins/__init__.py +23 -1
  47. meerschaum/utils/daemon/Daemon.py +89 -36
  48. meerschaum/utils/daemon/FileDescriptorInterceptor.py +140 -0
  49. meerschaum/utils/daemon/RotatingFile.py +130 -14
  50. meerschaum/utils/daemon/__init__.py +3 -0
  51. meerschaum/utils/dataframe.py +183 -8
  52. meerschaum/utils/dtypes/__init__.py +9 -5
  53. meerschaum/utils/formatting/_pipes.py +44 -10
  54. meerschaum/utils/misc.py +34 -2
  55. meerschaum/utils/packages/__init__.py +25 -8
  56. meerschaum/utils/packages/_packages.py +18 -20
  57. meerschaum/utils/process.py +13 -10
  58. meerschaum/utils/schedule.py +276 -30
  59. meerschaum/utils/threading.py +1 -0
  60. meerschaum/utils/typing.py +1 -1
  61. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/METADATA +59 -62
  62. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/RECORD +68 -66
  63. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/WHEEL +1 -1
  64. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/LICENSE +0 -0
  65. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/NOTICE +0 -0
  66. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/entry_points.txt +0 -0
  67. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/top_level.txt +0 -0
  68. {meerschaum-2.1.6.dist-info → meerschaum-2.2.0.dist-info}/zip-safe +0 -0
@@ -6,8 +6,10 @@
6
6
  Utility functions for working with data types.
7
7
  """
8
8
 
9
+ import traceback
9
10
  from decimal import Decimal, Context, InvalidOperation
10
11
  from meerschaum.utils.typing import Dict, Union, Any
12
+ from meerschaum.utils.warnings import warn
11
13
 
12
14
  MRSM_PD_DTYPES: Dict[str, str] = {
13
15
  'json': 'object',
@@ -37,9 +39,7 @@ def to_pandas_dtype(dtype: str) -> str:
37
39
  from meerschaum.utils.dtypes.sql import get_pd_type_from_db_type
38
40
  return get_pd_type_from_db_type(dtype)
39
41
 
40
- import traceback
41
42
  from meerschaum.utils.packages import attempt_import
42
- from meerschaum.utils.warnings import warn
43
43
  pandas = attempt_import('pandas', lazy=False)
44
44
 
45
45
  try:
@@ -88,8 +88,12 @@ def are_dtypes_equal(
88
88
  return False
89
89
  return True
90
90
 
91
- if ldtype == rdtype:
92
- return True
91
+ try:
92
+ if ldtype == rdtype:
93
+ return True
94
+ except Exception as e:
95
+ warn(f"Exception when comparing dtypes, returning False:\n{traceback.format_exc()}")
96
+ return False
93
97
 
94
98
  ### Sometimes pandas dtype objects are passed.
95
99
  ldtype = str(ldtype)
@@ -177,7 +181,7 @@ def attempt_cast_to_numeric(value: Any) -> Any:
177
181
  return value
178
182
 
179
183
 
180
- def value_is_null(value: Any) -> Any:
184
+ def value_is_null(value: Any) -> bool:
181
185
  """
182
186
  Determine if a value is a null-like string.
183
187
  """
@@ -9,7 +9,7 @@ Formatting functions for printing pipes
9
9
  from __future__ import annotations
10
10
  import json
11
11
  import meerschaum as mrsm
12
- from meerschaum.utils.typing import PipesDict, Dict, Union, Optional, SuccessTuple, Any
12
+ from meerschaum.utils.typing import PipesDict, Dict, Union, Optional, SuccessTuple, Any, List
13
13
  from meerschaum.config import get_config
14
14
 
15
15
  def pprint_pipes(pipes: PipesDict) -> None:
@@ -481,22 +481,54 @@ def print_pipes_results(
481
481
  )
482
482
 
483
483
 
484
- def extract_stats_from_message(message: str) -> Dict[str, int]:
484
+ def extract_stats_from_message(
485
+ message: str,
486
+ stat_keys: Optional[List[str]] = None,
487
+ ) -> Dict[str, int]:
485
488
  """
486
- Given a sync message, return the insert, update stats from within.
489
+ Given a sync message, return the insert, update, upsert stats from within.
490
+
491
+ Parameters
492
+ ----------
493
+ message: str
494
+ The message to parse for statistics.
495
+
496
+ stat_keys: Optional[List[str]], default None
497
+ If provided, search for these words (case insensitive) in the message.
498
+ Defaults to `['inserted', 'updated', 'upserted']`.
499
+
500
+ Returns
501
+ -------
502
+ A dictionary mapping the stat keys to the total number of rows affected.
487
503
  """
488
- stats = {
489
- 'inserted': 0,
490
- 'updated': 0,
491
- 'upserted': 0,
504
+ stat_keys = stat_keys or ['inserted', 'updated', 'upserted']
505
+ lines_stats = [extract_stats_from_line(line, stat_keys) for line in message.split('\n')]
506
+ message_stats = {
507
+ stat_key: sum(stats.get(stat_key, 0) for stats in lines_stats)
508
+ for stat_key in stat_keys
492
509
  }
510
+ return message_stats
493
511
 
494
- for search_key in list(stats.keys()):
495
- if search_key not in message.lower():
512
+
513
+ def extract_stats_from_line(
514
+ line: str,
515
+ stat_keys: List[str],
516
+ ) -> Dict[str, int]:
517
+ """
518
+ Return the insert, update, upsert stats from a single line.
519
+ """
520
+ stats = {key: 0 for key in stat_keys}
521
+
522
+ for stat_key in stat_keys:
523
+ search_key = stat_key.lower()
524
+ if search_key not in line.lower():
496
525
  continue
497
526
 
498
527
  ### stat_text starts with the digits we want.
499
- stat_text = message.lower().split(search_key + ' ')[1]
528
+ try:
529
+ stat_text = line.lower().split(search_key + ' ')[1]
530
+ except IndexError:
531
+ continue
500
532
 
501
533
  ### find the first non-digit value.
502
534
  end_of_num_ix = -1
@@ -504,6 +536,8 @@ def extract_stats_from_message(message: str) -> Dict[str, int]:
504
536
  if not char.isdigit():
505
537
  end_of_num_ix = i
506
538
  break
539
+ if i == len(stat_text) - 1:
540
+ end_of_num_ix = i + 1
507
541
  if end_of_num_ix == -1:
508
542
  continue
509
543
 
meerschaum/utils/misc.py CHANGED
@@ -1234,7 +1234,7 @@ def truncate_string_sections(item: str, delimeter: str = '_', max_len: int = 128
1234
1234
 
1235
1235
 
1236
1236
  def separate_negation_values(
1237
- vals: List[str],
1237
+ vals: Union[List[str], Tuple[str]],
1238
1238
  negation_prefix: Optional[str] = None,
1239
1239
  ) -> Tuple[List[str], List[str]]:
1240
1240
  """
@@ -1243,7 +1243,7 @@ def separate_negation_values(
1243
1243
 
1244
1244
  Parameters
1245
1245
  ----------
1246
- vals: List[str]
1246
+ vals: Union[List[str], Tuple[str]]
1247
1247
  A list of strings to parse.
1248
1248
 
1249
1249
  negation_prefix: Optional[str], default None
@@ -1263,6 +1263,38 @@ def separate_negation_values(
1263
1263
  return _in_vals, _ex_vals
1264
1264
 
1265
1265
 
1266
+ def get_in_ex_params(params: Optional[Dict[str, Any]]) -> Dict[str, Tuple[List[Any], List[Any]]]:
1267
+ """
1268
+ Translate a params dictionary into lists of include- and exclude-values.
1269
+
1270
+ Parameters
1271
+ ----------
1272
+ params: Optional[Dict[str, Any]]
1273
+ A params query dictionary.
1274
+
1275
+ Returns
1276
+ -------
1277
+ A dictionary mapping keys to a tuple of lists for include and exclude values.
1278
+
1279
+ Examples
1280
+ --------
1281
+ >>> get_in_ex_params({'a': ['b', 'c', '_d', 'e', '_f']})
1282
+ {'a': (['b', 'c', 'e'], ['d', 'f'])}
1283
+ """
1284
+ if not params:
1285
+ return {}
1286
+ return {
1287
+ col: separate_negation_values(
1288
+ (
1289
+ val
1290
+ if isinstance(val, (list, tuple))
1291
+ else [val]
1292
+ )
1293
+ )
1294
+ for col, val in params.items()
1295
+ }
1296
+
1297
+
1266
1298
  def flatten_list(list_: List[Any]) -> List[Any]:
1267
1299
  """
1268
1300
  Recursively flatten a list.
@@ -8,7 +8,7 @@ Functions for managing packages and virtual environments reside here.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- import importlib.util, os, pathlib
11
+ import importlib.util, os, pathlib, re
12
12
  from meerschaum.utils.typing import Any, List, SuccessTuple, Optional, Union, Tuple, Dict, Iterable
13
13
  from meerschaum.utils.threading import Lock, RLock
14
14
  from meerschaum.utils.packages._packages import packages, all_packages, get_install_names
@@ -35,6 +35,7 @@ _locks = {
35
35
  }
36
36
  _checked_for_updates = set()
37
37
  _is_installed_first_check: Dict[str, bool] = {}
38
+ _MRSM_PACKAGE_ARCHIVES_PREFIX: str = "https://meerschaum.io/files/archives/"
38
39
 
39
40
  def get_module_path(
40
41
  import_name: str,
@@ -350,6 +351,7 @@ def determine_version(
350
351
  with _locks['import_versions']:
351
352
  if venv not in import_versions:
352
353
  import_versions[venv] = {}
354
+ import importlib.metadata
353
355
  import re, os
354
356
  old_cwd = os.getcwd()
355
357
  if debug:
@@ -639,6 +641,15 @@ def need_update(
639
641
 
640
642
  ### We might be depending on a prerelease.
641
643
  ### Sanity check that the required version is not greater than the installed version.
644
+ required_version = (
645
+ required_version.replace(_MRSM_PACKAGE_ARCHIVES_PREFIX, '')
646
+ .replace(' @ ', '').replace('wheels', '').replace('+mrsm', '').replace('/-', '')
647
+ .replace('-py3-none-any.whl', '')
648
+ )
649
+
650
+ if 'a' in required_version:
651
+ required_version = required_version.replace('a', '-dev').replace('+mrsm', '')
652
+ version = version.replace('a', '-dev').replace('+mrsm', '')
642
653
  try:
643
654
  return (
644
655
  (not semver.Version.parse(version).match(required_version))
@@ -818,8 +829,11 @@ def pip_install(
818
829
  check_wheel = False, debug = debug,
819
830
  ):
820
831
  warn(
821
- f"Failed to install `setuptools` and `wheel` for virtual environment '{venv}'.",
822
- color=False,
832
+ (
833
+ "Failed to install `setuptools` and `wheel` for virtual "
834
+ + f"environment '{venv}'."
835
+ ),
836
+ color = False,
823
837
  )
824
838
 
825
839
  if requirements_file_path is not None:
@@ -882,13 +896,16 @@ def pip_install(
882
896
  f"Failed to clean up package '{_install_no_version}'.",
883
897
  )
884
898
 
885
- success = run_python_package(
899
+ rc = run_python_package(
886
900
  'pip',
887
901
  _args + _packages,
888
902
  venv = venv,
889
903
  env = _get_pip_os_env(),
890
904
  debug = debug,
891
- ) == 0
905
+ )
906
+ if debug:
907
+ print(f"{rc=}")
908
+ success = rc == 0
892
909
 
893
910
  msg = (
894
911
  "Successfully " + ('un' if _uninstall else '') + "installed packages." if success
@@ -1379,13 +1396,13 @@ def get_modules_from_package(
1379
1396
  Returns
1380
1397
  -------
1381
1398
  Either list of modules or tuple of lists.
1382
-
1383
1399
  """
1384
1400
  from os.path import dirname, join, isfile, isdir, basename
1385
1401
  import glob
1386
1402
 
1387
1403
  pattern = '*' if recursive else '*.py'
1388
- module_names = glob.glob(join(dirname(package.__file__), pattern), recursive=recursive)
1404
+ package_path = dirname(package.__file__ or package.__path__[0])
1405
+ module_names = glob.glob(join(package_path, pattern), recursive=recursive)
1389
1406
  _all = [
1390
1407
  basename(f)[:-3] if isfile(f) else basename(f)
1391
1408
  for f in module_names
@@ -1410,7 +1427,7 @@ def get_modules_from_package(
1410
1427
  modules.append(m)
1411
1428
  except Exception as e:
1412
1429
  if debug:
1413
- dprint(e)
1430
+ dprint(str(e))
1414
1431
  finally:
1415
1432
  if modules_venvs:
1416
1433
  deactivate_venv(module_name.split('.')[-1], debug=debug)
@@ -30,7 +30,7 @@ packages: Dict[str, Dict[str, str]] = {
30
30
  'more_termcolor' : 'more-termcolor>=1.1.3',
31
31
  'humanfriendly' : 'humanfriendly>=10.0.0',
32
32
  },
33
- '_required': {
33
+ 'core': {
34
34
  'wheel' : 'wheel>=0.34.2',
35
35
  'setuptools' : 'setuptools>=63.3.0',
36
36
  'yaml' : 'PyYAML>=5.3.1',
@@ -49,21 +49,21 @@ packages: Dict[str, Dict[str, str]] = {
49
49
  'daemon' : 'python-daemon>=0.2.3',
50
50
  'fasteners' : 'fasteners>=0.18.0',
51
51
  'psutil' : 'psutil>=5.8.0',
52
- 'watchgod' : 'watchgod>=0.7.0',
52
+ 'watchfiles' : 'watchfiles>=0.21.0',
53
53
  'dill' : 'dill>=0.3.3',
54
54
  'virtualenv' : 'virtualenv>=20.1.0',
55
- 'rocketry' : 'rocketry>=2.5.1',
55
+ 'apscheduler' : 'APScheduler>=4.0.0a5',
56
56
  },
57
57
  'drivers': {
58
58
  'cryptography' : 'cryptography>=38.0.1',
59
- 'psycopg2' : 'psycopg2-binary>=2.8.6',
59
+ 'psycopg' : 'psycopg[binary]>=3.1.18',
60
60
  'pymysql' : 'PyMySQL>=0.9.0',
61
61
  'aiomysql' : 'aiomysql>=0.0.21',
62
62
  'sqlalchemy_cockroachdb' : 'sqlalchemy-cockroachdb>=2.0.0',
63
- 'duckdb' : 'duckdb>=0.9.0',
63
+ 'duckdb' : 'duckdb<0.10.3',
64
64
  'duckdb_engine' : 'duckdb-engine>=0.9.2',
65
65
  },
66
- '_drivers': {
66
+ 'drivers-extras': {
67
67
  'pyodbc' : 'pyodbc>=4.0.30',
68
68
  'cx_Oracle' : 'cx_Oracle>=8.3.0',
69
69
  },
@@ -75,11 +75,11 @@ packages: Dict[str, Dict[str, str]] = {
75
75
  'gadwall' : 'gadwall>=0.2.0',
76
76
  },
77
77
  'stack': {
78
- 'compose' : 'docker-compose>=1.27.4',
78
+ 'compose' : 'docker-compose>=1.29.2',
79
79
  },
80
80
  'build': {
81
- 'cx_Freeze' : 'cx_Freeze>=6.5.1',
82
- 'PyInstaller' : 'pyinstaller>=5.0.0-dev0',
81
+ 'cx_Freeze' : 'cx_Freeze>=7.0.0',
82
+ 'PyInstaller' : 'pyinstaller>6.6.0',
83
83
  },
84
84
  'dev-tools': {
85
85
  'twine' : 'twine>=3.2.0',
@@ -89,6 +89,7 @@ packages: Dict[str, Dict[str, str]] = {
89
89
  'pytest' : 'pytest>=6.2.2',
90
90
  'pytest_xdist' : 'pytest-xdist>=3.2.1',
91
91
  'heartrate' : 'heartrate>=0.2.1',
92
+ 'build' : 'build>=1.2.1',
92
93
  },
93
94
  'setup': {
94
95
  },
@@ -119,8 +120,8 @@ packages: Dict[str, Dict[str, str]] = {
119
120
  packages['sql'] = {
120
121
  'numpy' : 'numpy>=1.18.5',
121
122
  'pandas' : 'pandas[parquet]>=2.0.1',
122
- 'pyarrow' : 'pyarrow>=7.0.0',
123
- 'dask' : 'dask>=2023.5.0',
123
+ 'pyarrow' : 'pyarrow>=16.1.0',
124
+ 'dask' : 'dask[dataframe]>=2024.5.1',
124
125
  'pytz' : 'pytz',
125
126
  'joblib' : 'joblib>=0.17.0',
126
127
  'sqlalchemy' : 'SQLAlchemy>=2.0.5',
@@ -129,7 +130,7 @@ packages['sql'] = {
129
130
  'asyncpg' : 'asyncpg>=0.21.0',
130
131
  }
131
132
  packages['sql'].update(packages['drivers'])
132
- packages['sql'].update(packages['_required'])
133
+ packages['sql'].update(packages['core'])
133
134
  packages['dash'] = {
134
135
  'flask_compress' : 'Flask-Compress>=1.10.1',
135
136
  'dash' : 'dash>=2.6.2',
@@ -141,17 +142,14 @@ packages['dash'] = {
141
142
  'tornado' : 'tornado>=6.1.0',
142
143
  }
143
144
  packages['api'] = {
144
- 'uvicorn' : 'uvicorn[standard]>=0.22.0',
145
- 'gunicorn' : 'gunicorn>=20.1.0',
145
+ 'uvicorn' : 'uvicorn[standard]>=0.29.0',
146
+ 'gunicorn' : 'gunicorn>=22.0.0',
146
147
  'dotenv' : 'python-dotenv>=0.20.0',
147
148
  'websockets' : 'websockets>=11.0.3',
148
- 'fastapi' : 'fastapi>=0.100.0',
149
- 'passlib' : 'passlib>=1.7.4',
149
+ 'fastapi' : 'fastapi>=0.111.0',
150
150
  'fastapi_login' : 'fastapi-login>=1.7.2',
151
- 'multipart' : 'python-multipart>=0.0.5',
152
- 'pydantic' : 'pydantic<2.0.0',
151
+ 'multipart' : 'python-multipart>=0.0.9',
153
152
  'httpx' : 'httpx>=0.24.1',
154
- 'websockets' : 'websockets>=11.0.3',
155
153
  }
156
154
  packages['api'].update(packages['sql'])
157
155
  packages['api'].update(packages['formatting'])
@@ -171,7 +169,7 @@ def get_install_names():
171
169
  install_names[get_install_no_version(_install_name)] = _import_name
172
170
  return install_names
173
171
 
174
- skip_groups = {'docs', 'build', 'cli', 'dev-tools', 'portable', 'extras', 'stack', '_drivers'}
172
+ skip_groups = {'docs', 'build', 'cli', 'dev-tools', 'portable', 'extras', 'stack', 'drivers-extras'}
175
173
  full = []
176
174
  _full = {}
177
175
  for group, import_names in packages.items():
@@ -11,6 +11,7 @@ See `meerschaum.utils.pool` for multiprocessing and
11
11
  from __future__ import annotations
12
12
  import os, signal, subprocess, sys, platform
13
13
  from meerschaum.utils.typing import Union, Optional, Any, Callable, Dict, Tuple
14
+ from meerschaum.config.static import STATIC_CONFIG
14
15
 
15
16
  def run_process(
16
17
  *args,
@@ -68,9 +69,18 @@ def run_process(
68
69
  if platform.system() == 'Windows':
69
70
  foreground = False
70
71
 
71
- if line_callback is not None:
72
+ def print_line(line):
73
+ sys.stdout.write(line.decode('utf-8'))
74
+ sys.stdout.flush()
75
+
76
+ if capture_output or line_callback is not None:
77
+ kw['stdout'] = subprocess.PIPE
78
+ kw['stderr'] = subprocess.STDOUT
79
+ elif os.environ.get(STATIC_CONFIG['environment']['daemon_id']):
72
80
  kw['stdout'] = subprocess.PIPE
73
81
  kw['stderr'] = subprocess.STDOUT
82
+ if line_callback is None:
83
+ line_callback = print_line
74
84
 
75
85
  if 'env' not in kw:
76
86
  kw['env'] = os.environ
@@ -112,15 +122,6 @@ def run_process(
112
122
  kw['preexec_fn'] = new_pgid
113
123
 
114
124
  try:
115
- # fork the child
116
- # stdout, stderr = (
117
- # (sys.stdout, sys.stderr) if not capture_output
118
- # else (subprocess.PIPE, subprocess.PIPE)
119
- # )
120
- if capture_output:
121
- kw['stdout'] = subprocess.PIPE
122
- kw['stderr'] = subprocess.PIPE
123
-
124
125
  child = subprocess.Popen(*args, **kw)
125
126
 
126
127
  # we can't set the process group id from the parent since the child
@@ -197,6 +198,8 @@ def poll_process(
197
198
  while proc.poll() is None:
198
199
  line = proc.stdout.readline()
199
200
  line_callback(line)
201
+
200
202
  if timeout_seconds is not None:
201
203
  watchdog_thread.cancel()
204
+
202
205
  return proc.poll()