meerschaum 2.7.4__py3-none-any.whl → 2.7.6__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 (34) hide show
  1. meerschaum/_internal/shell/Shell.py +4 -6
  2. meerschaum/_internal/shell/ShellCompleter.py +6 -5
  3. meerschaum/actions/clear.py +6 -3
  4. meerschaum/actions/copy.py +33 -27
  5. meerschaum/actions/sql.py +14 -4
  6. meerschaum/actions/sync.py +22 -18
  7. meerschaum/api/dash/pipes.py +2 -3
  8. meerschaum/config/_default.py +11 -0
  9. meerschaum/config/_version.py +1 -1
  10. meerschaum/connectors/api/_misc.py +3 -2
  11. meerschaum/connectors/api/_pipes.py +8 -9
  12. meerschaum/connectors/sql/_SQLConnector.py +1 -0
  13. meerschaum/connectors/sql/_cli.py +18 -12
  14. meerschaum/connectors/sql/_create_engine.py +1 -0
  15. meerschaum/connectors/sql/_pipes.py +51 -14
  16. meerschaum/connectors/sql/_sql.py +109 -16
  17. meerschaum/jobs/_Job.py +1 -0
  18. meerschaum/plugins/__init__.py +7 -3
  19. meerschaum/utils/daemon/Daemon.py +11 -3
  20. meerschaum/utils/daemon/__init__.py +2 -2
  21. meerschaum/utils/misc.py +7 -6
  22. meerschaum/utils/packages/__init__.py +35 -28
  23. meerschaum/utils/packages/_packages.py +1 -1
  24. meerschaum/utils/prompt.py +54 -36
  25. meerschaum/utils/venv/_Venv.py +6 -1
  26. meerschaum/utils/venv/__init__.py +32 -16
  27. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/METADATA +4 -4
  28. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/RECORD +34 -34
  29. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/WHEEL +1 -1
  30. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/LICENSE +0 -0
  31. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/NOTICE +0 -0
  32. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/entry_points.txt +0 -0
  33. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/top_level.txt +0 -0
  34. {meerschaum-2.7.4.dist-info → meerschaum-2.7.6.dist-info}/zip-safe +0 -0
@@ -6,8 +6,10 @@ This module contains SQLConnector functions for executing SQL queries.
6
6
  """
7
7
 
8
8
  from __future__ import annotations
9
+
10
+ import meerschaum as mrsm
9
11
  from meerschaum.utils.typing import (
10
- Union, Mapping, List, Dict, SuccessTuple, Optional, Any, Iterable, Callable,
12
+ Union, List, Dict, SuccessTuple, Optional, Any, Iterable, Callable,
11
13
  Tuple, Hashable,
12
14
  )
13
15
 
@@ -15,7 +17,7 @@ from meerschaum.utils.debug import dprint
15
17
  from meerschaum.utils.warnings import warn
16
18
 
17
19
  ### database flavors that can use bulk insert
18
- _bulk_flavors = {'postgresql', 'timescaledb', 'citus'}
20
+ _bulk_flavors = {'postgresql', 'timescaledb', 'citus', 'mssql'}
19
21
  ### flavors that do not support chunks
20
22
  _disallow_chunks_flavors = ['duckdb']
21
23
  _max_chunks_flavors = {'sqlite': 1000}
@@ -776,9 +778,11 @@ def to_sql(
776
778
  import time
777
779
  import json
778
780
  from decimal import Decimal
781
+ from datetime import timedelta
779
782
  from meerschaum.utils.warnings import error, warn
780
783
  import warnings
781
784
  import functools
785
+
782
786
  if name is None:
783
787
  error(f"Name must not be `None` to insert data into {self}.")
784
788
 
@@ -805,6 +809,7 @@ def to_sql(
805
809
  quantize_decimal,
806
810
  coerce_timezone,
807
811
  encode_bytes_for_bytea,
812
+ serialize_bytes,
808
813
  )
809
814
  from meerschaum.utils.dtypes.sql import (
810
815
  NUMERIC_PRECISION_FLAVORS,
@@ -812,6 +817,7 @@ def to_sql(
812
817
  PD_TO_SQLALCHEMY_DTYPES_FLAVORS,
813
818
  get_db_type_from_pd_type,
814
819
  )
820
+ from meerschaum.utils.misc import interval_str
815
821
  from meerschaum.connectors.sql._create_engine import flavor_configs
816
822
  from meerschaum.utils.packages import attempt_import, import_pandas
817
823
  sqlalchemy = attempt_import('sqlalchemy', debug=debug)
@@ -821,24 +827,36 @@ def to_sql(
821
827
  bytes_cols = get_bytes_cols(df)
822
828
  numeric_cols = get_numeric_cols(df)
823
829
 
824
- stats = {'target': name,}
830
+ enable_bulk_insert = mrsm.get_config(
831
+ 'system', 'connectors', 'sql', 'bulk_insert'
832
+ ).get(self.flavor, False)
833
+ stats = {'target': name}
825
834
  ### resort to defaults if None
826
835
  copied = False
827
- use_psql_copy = False
836
+ use_bulk_insert = False
828
837
  if method == "":
829
- if self.flavor in _bulk_flavors:
830
- method = functools.partial(psql_insert_copy, schema=self.schema)
831
- use_psql_copy = True
838
+ if enable_bulk_insert:
839
+ method = (
840
+ functools.partial(mssql_insert_json, debug=debug)
841
+ if self.flavor == 'mssql'
842
+ else functools.partial(psql_insert_copy, debug=debug)
843
+ )
844
+ use_bulk_insert = True
832
845
  else:
833
846
  ### Should resolve to 'multi' or `None`.
834
847
  method = flavor_configs.get(self.flavor, {}).get('to_sql', {}).get('method', 'multi')
835
848
 
836
- if bytes_cols and (use_psql_copy or self.flavor == 'oracle'):
849
+ if bytes_cols and (use_bulk_insert or self.flavor == 'oracle'):
837
850
  if safe_copy and not copied:
838
851
  df = df.copy()
839
852
  copied = True
853
+ bytes_serializer = (
854
+ functools.partial(encode_bytes_for_bytea, with_prefix=(self.flavor != 'oracle'))
855
+ if self.flavor != 'mssql'
856
+ else serialize_bytes
857
+ )
840
858
  for col in bytes_cols:
841
- df[col] = df[col].apply(encode_bytes_for_bytea, with_prefix=(self.flavor != 'oracle'))
859
+ df[col] = df[col].apply(bytes_serializer)
842
860
 
843
861
  if self.flavor in NUMERIC_AS_TEXT_FLAVORS:
844
862
  if safe_copy and not copied:
@@ -982,13 +1000,19 @@ def to_sql(
982
1000
 
983
1001
  end = time.perf_counter()
984
1002
  if success:
985
- msg = f"It took {round(end - start, 2)} seconds to sync {len(df)} rows to {name}."
1003
+ num_rows = len(df)
1004
+ msg = (
1005
+ f"It took {interval_str(timedelta(seconds=(end - start)))} "
1006
+ + f"to sync {num_rows:,} row"
1007
+ + ('s' if num_rows != 1 else '')
1008
+ + f" to {name}."
1009
+ )
986
1010
  stats['start'] = start
987
1011
  stats['end'] = end
988
1012
  stats['duration'] = end - start
989
1013
 
990
1014
  if debug:
991
- print(f" done.", flush=True)
1015
+ print(" done.", flush=True)
992
1016
  dprint(msg)
993
1017
 
994
1018
  stats['success'] = success
@@ -1005,7 +1029,7 @@ def psql_insert_copy(
1005
1029
  conn: Union[sqlalchemy.engine.Engine, sqlalchemy.engine.Connection],
1006
1030
  keys: List[str],
1007
1031
  data_iter: Iterable[Any],
1008
- schema: Optional[str] = None,
1032
+ debug: bool = False,
1009
1033
  ) -> None:
1010
1034
  """
1011
1035
  Execute SQL statement inserting data for PostgreSQL.
@@ -1022,18 +1046,15 @@ def psql_insert_copy(
1022
1046
  data_iter: Iterable[Any]
1023
1047
  Iterable that iterates the values to be inserted
1024
1048
 
1025
- schema: Optional[str], default None
1026
- Optionally specify the schema of the table to be inserted into.
1027
-
1028
1049
  Returns
1029
1050
  -------
1030
1051
  None
1031
1052
  """
1032
1053
  import csv
1033
- from io import StringIO
1034
1054
  import json
1035
1055
 
1036
1056
  from meerschaum.utils.sql import sql_item_name
1057
+ from meerschaum.utils.warnings import dprint
1037
1058
 
1038
1059
  ### NOTE: PostgreSQL doesn't support NUL chars in text, so they're removed from strings.
1039
1060
  data_iter = (
@@ -1057,6 +1078,8 @@ def psql_insert_copy(
1057
1078
  table_name = sql_item_name(table.name, 'postgresql', table.schema)
1058
1079
  columns = ', '.join(f'"{k}"' for k in keys)
1059
1080
  sql = f"COPY {table_name} ({columns}) FROM STDIN WITH CSV NULL '\\N'"
1081
+ if debug:
1082
+ dprint(sql)
1060
1083
 
1061
1084
  dbapi_conn = conn.connection
1062
1085
  with dbapi_conn.cursor() as cur:
@@ -1065,6 +1088,76 @@ def psql_insert_copy(
1065
1088
  writer.writerows(data_iter)
1066
1089
 
1067
1090
 
1091
+ def mssql_insert_json(
1092
+ table: pandas.io.sql.SQLTable,
1093
+ conn: Union[sqlalchemy.engine.Engine, sqlalchemy.engine.Connection],
1094
+ keys: List[str],
1095
+ data_iter: Iterable[Any],
1096
+ cols_types: Optional[Dict[str, str]] = None,
1097
+ debug: bool = False,
1098
+ ):
1099
+ """
1100
+ Execute SQL statement inserting data via OPENJSON.
1101
+
1102
+ Adapted from this snippet:
1103
+ https://gist.github.com/gordthompson/1fb0f1c3f5edbf6192e596de8350f205
1104
+
1105
+ Parameters
1106
+ ----------
1107
+ table: pandas.io.sql.SQLTable
1108
+
1109
+ conn: Union[sqlalchemy.engine.Engine, sqlalchemy.engine.Connection]
1110
+
1111
+ keys: List[str]
1112
+ Column names
1113
+
1114
+ data_iter: Iterable[Any]
1115
+ Iterable that iterates the values to be inserted
1116
+
1117
+ cols_types: Optional[Dict[str, str]], default None
1118
+ If provided, use these as the columns and types for the table.
1119
+
1120
+ Returns
1121
+ -------
1122
+ None
1123
+ """
1124
+ import json
1125
+ from meerschaum.utils.sql import sql_item_name
1126
+ from meerschaum.utils.dtypes.sql import get_pd_type_from_db_type, get_db_type_from_pd_type
1127
+ from meerschaum.utils.warnings import dprint
1128
+ table_name = sql_item_name(table.name, 'mssql', table.schema)
1129
+ if not cols_types:
1130
+ pd_types = {
1131
+ str(column.name): get_pd_type_from_db_type(str(column.type))
1132
+ for column in table.table.columns
1133
+ }
1134
+ cols_types = {
1135
+ col: get_db_type_from_pd_type(typ, 'mssql')
1136
+ for col, typ in pd_types.items()
1137
+ }
1138
+ columns = ",\n ".join([f"[{k}]" for k in keys])
1139
+ json_data = [dict(zip(keys, row)) for row in data_iter]
1140
+ with_clause = ",\n ".join(
1141
+ [
1142
+ f"[{col_name}] {col_type} '$.\"{col_name}\"'"
1143
+ for col_name, col_type in cols_types.items()
1144
+ ]
1145
+ )
1146
+ placeholder = "?" if conn.dialect.paramstyle == "qmark" else "%s"
1147
+ sql = (
1148
+ f"INSERT INTO {table_name} (\n {columns}\n)\n"
1149
+ f"SELECT\n {columns}\n"
1150
+ f"FROM OPENJSON({placeholder})\n"
1151
+ "WITH (\n"
1152
+ f" {with_clause}\n"
1153
+ ");"
1154
+ )
1155
+ if debug:
1156
+ dprint(sql)
1157
+
1158
+ conn.exec_driver_sql(sql, (json.dumps(json_data, default=str),))
1159
+
1160
+
1068
1161
  def format_sql_query_for_dask(query: str) -> 'sqlalchemy.sql.selectable.Select':
1069
1162
  """
1070
1163
  Given a `SELECT` query, return a `sqlalchemy` query for Dask to use.
meerschaum/jobs/_Job.py CHANGED
@@ -313,6 +313,7 @@ class Job:
313
313
  if not cleanup_success:
314
314
  return cleanup_success, cleanup_msg
315
315
 
316
+ _ = self.daemon._properties.pop('result', None)
316
317
  return cleanup_success, f"Deleted {self}."
317
318
 
318
319
  def is_running(self) -> bool:
@@ -126,8 +126,8 @@ def pre_sync_hook(
126
126
 
127
127
 
128
128
  def post_sync_hook(
129
- function: Callable[[Any], Any],
130
- ) -> Callable[[Any], Any]:
129
+ function: Callable[[Any], Any],
130
+ ) -> Callable[[Any], Any]:
131
131
  """
132
132
  Register a function as a sync hook to be executed upon completion of a sync.
133
133
 
@@ -143,10 +143,14 @@ def post_sync_hook(
143
143
  Examples
144
144
  --------
145
145
  >>> from meerschaum.plugins import post_sync_hook
146
+ >>> from meerschaum.utils.misc import interval_str
147
+ >>> from datetime import timedelta
146
148
  >>>
147
149
  >>> @post_sync_hook
148
150
  ... def log_sync(pipe, success_tuple, duration=None, **kwargs):
149
- ... print(f"It took {round(duration, 2)} seconds to sync {pipe}.")
151
+ ... duration_delta = timedelta(seconds=duration)
152
+ ... duration_text = interval_str(duration_delta)
153
+ ... print(f"It took {duration_text} to sync {pipe}.")
150
154
  >>>
151
155
  """
152
156
  with _locks['_post_sync_hooks']:
@@ -261,7 +261,10 @@ class Daemon:
261
261
  -------
262
262
  Nothing — this will exit the parent process.
263
263
  """
264
- import platform, sys, os, traceback
264
+ import platform
265
+ import sys
266
+ import os
267
+ import traceback
265
268
  from meerschaum.utils.warnings import warn
266
269
  from meerschaum.config import get_config
267
270
  daemon = attempt_import('daemon')
@@ -1014,7 +1017,8 @@ class Daemon:
1014
1017
 
1015
1018
  def read_pickle(self) -> Daemon:
1016
1019
  """Read a Daemon's pickle file and return the `Daemon`."""
1017
- import pickle, traceback
1020
+ import pickle
1021
+ import traceback
1018
1022
  if not self.pickle_path.exists():
1019
1023
  error(f"Pickle file does not exist for daemon '{self.daemon_id}'.")
1020
1024
 
@@ -1050,6 +1054,9 @@ class Daemon:
1050
1054
  if self._properties is None:
1051
1055
  self._properties = {}
1052
1056
 
1057
+ if self._properties.get('result', None) is None:
1058
+ _ = self._properties.pop('result', None)
1059
+
1053
1060
  if _file_properties is not None:
1054
1061
  self._properties = apply_patch_to_config(
1055
1062
  _file_properties,
@@ -1085,7 +1092,8 @@ class Daemon:
1085
1092
 
1086
1093
  def write_pickle(self) -> SuccessTuple:
1087
1094
  """Write the pickle file for the daemon."""
1088
- import pickle, traceback
1095
+ import pickle
1096
+ import traceback
1089
1097
  try:
1090
1098
  self.path.mkdir(parents=True, exist_ok=True)
1091
1099
  with open(self.pickle_path, 'wb+') as pickle_file:
@@ -37,7 +37,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
37
37
  filtered_sysargs = [arg for arg in sysargs if arg not in ('-d', '--daemon')]
38
38
  try:
39
39
  label = shlex.join(filtered_sysargs) if sysargs else None
40
- except Exception as e:
40
+ except Exception:
41
41
  label = ' '.join(filtered_sysargs) if sysargs else None
42
42
 
43
43
  name = _args.get('name', None)
@@ -45,7 +45,7 @@ def daemon_entry(sysargs: Optional[List[str]] = None) -> SuccessTuple:
45
45
  if name:
46
46
  try:
47
47
  daemon = Daemon(daemon_id=name)
48
- except Exception as e:
48
+ except Exception:
49
49
  daemon = None
50
50
 
51
51
  if daemon is not None:
meerschaum/utils/misc.py CHANGED
@@ -90,20 +90,21 @@ def add_method_to_class(
90
90
 
91
91
 
92
92
  def generate_password(length: int = 12) -> str:
93
- """Generate a secure password of given length.
93
+ """
94
+ Generate a secure password of given length.
94
95
 
95
96
  Parameters
96
97
  ----------
97
- length : int, default 12
98
+ length: int, default 12
98
99
  The length of the password.
99
100
 
100
101
  Returns
101
102
  -------
102
103
  A random password string.
103
-
104
104
  """
105
- import secrets, string
106
- return ''.join((secrets.choice(string.ascii_letters) for i in range(length)))
105
+ import secrets
106
+ import string
107
+ return ''.join((secrets.choice(string.ascii_letters + string.digits) for i in range(length)))
107
108
 
108
109
  def is_int(s : str) -> bool:
109
110
  """
@@ -121,7 +122,7 @@ def is_int(s : str) -> bool:
121
122
  """
122
123
  try:
123
124
  float(s)
124
- except Exception as e:
125
+ except Exception:
125
126
  return False
126
127
 
127
128
  return float(s).is_integer()
@@ -337,14 +337,14 @@ def get_install_no_version(install_name: str) -> str:
337
337
 
338
338
  import_versions = {}
339
339
  def determine_version(
340
- path: pathlib.Path,
341
- import_name: Optional[str] = None,
342
- venv: Optional[str] = 'mrsm',
343
- search_for_metadata: bool = True,
344
- split: bool = True,
345
- warn: bool = False,
346
- debug: bool = False,
347
- ) -> Union[str, None]:
340
+ path: pathlib.Path,
341
+ import_name: Optional[str] = None,
342
+ venv: Optional[str] = 'mrsm',
343
+ search_for_metadata: bool = True,
344
+ split: bool = True,
345
+ warn: bool = False,
346
+ debug: bool = False,
347
+ ) -> Union[str, None]:
348
348
  """
349
349
  Determine a module's `__version__` string from its filepath.
350
350
 
@@ -381,11 +381,8 @@ def determine_version(
381
381
  with _locks['import_versions']:
382
382
  if venv not in import_versions:
383
383
  import_versions[venv] = {}
384
- import importlib.metadata
385
- import re, os
384
+ import os
386
385
  old_cwd = os.getcwd()
387
- if debug:
388
- from meerschaum.utils.debug import dprint
389
386
  from meerschaum.utils.warnings import warn as warn_function
390
387
  if import_name is None:
391
388
  import_name = path.parent.stem if path.stem == '__init__' else path.stem
@@ -395,7 +392,10 @@ def determine_version(
395
392
  _version = None
396
393
  module_parent_dir = (
397
394
  path.parent.parent if path.stem == '__init__' else path.parent
398
- ) if path is not None else venv_target_path(venv, debug=debug)
395
+ ) if path is not None else venv_target_path(venv, allow_nonexistent=True, debug=debug)
396
+
397
+ if not module_parent_dir.exists():
398
+ return None
399
399
 
400
400
  installed_dir_name = _import_to_dir_name(import_name)
401
401
  clean_installed_dir_name = installed_dir_name.lower().replace('-', '_')
@@ -403,7 +403,11 @@ def determine_version(
403
403
  ### First, check if a dist-info directory exists.
404
404
  _found_versions = []
405
405
  if search_for_metadata:
406
- for filename in os.listdir(module_parent_dir):
406
+ try:
407
+ filenames = os.listdir(module_parent_dir)
408
+ except FileNotFoundError:
409
+ filenames = []
410
+ for filename in filenames:
407
411
  if not filename.endswith('.dist-info'):
408
412
  continue
409
413
  filename_lower = filename.lower()
@@ -430,7 +434,7 @@ def determine_version(
430
434
  try:
431
435
  os.chdir(module_parent_dir)
432
436
  _version = importlib_metadata.metadata(import_name)['Version']
433
- except Exception as e:
437
+ except Exception:
434
438
  _version = None
435
439
  finally:
436
440
  os.chdir(old_cwd)
@@ -698,7 +702,7 @@ def need_update(
698
702
  (not semver.Version.parse(version).match(required_version))
699
703
  if required_version else False
700
704
  )
701
- except AttributeError as e:
705
+ except AttributeError:
702
706
  pip_install(_import_to_install_name('semver'), venv='mrsm', debug=debug)
703
707
  semver = manually_import_module('semver', venv='mrsm', debug=debug)
704
708
  return (
@@ -724,10 +728,10 @@ def need_update(
724
728
 
725
729
 
726
730
  def get_pip(
727
- venv: Optional[str] = 'mrsm',
728
- color: bool = True,
729
- debug: bool = False,
730
- ) -> bool:
731
+ venv: Optional[str] = 'mrsm',
732
+ color: bool = True,
733
+ debug: bool = False,
734
+ ) -> bool:
731
735
  """
732
736
  Download and run the get-pip.py script.
733
737
 
@@ -747,7 +751,8 @@ def get_pip(
747
751
  A bool indicating success.
748
752
 
749
753
  """
750
- import sys, subprocess
754
+ import sys
755
+ import subprocess
751
756
  from meerschaum.utils.misc import wget
752
757
  from meerschaum.config._paths import CACHE_RESOURCES_PATH
753
758
  from meerschaum.config.static import STATIC_CONFIG
@@ -755,7 +760,7 @@ def get_pip(
755
760
  dest = CACHE_RESOURCES_PATH / 'get-pip.py'
756
761
  try:
757
762
  wget(url, dest, color=False, debug=debug)
758
- except Exception as e:
763
+ except Exception:
759
764
  print(f"Failed to fetch pip from '{url}'. Please install pip and restart Meerschaum.")
760
765
  sys.exit(1)
761
766
  if venv is not None:
@@ -776,6 +781,7 @@ def pip_install(
776
781
  _uninstall: bool = False,
777
782
  _from_completely_uninstall: bool = False,
778
783
  _install_uv_pip: bool = True,
784
+ _use_uv_pip: bool = True,
779
785
  color: bool = True,
780
786
  silent: bool = False,
781
787
  debug: bool = False,
@@ -831,15 +837,11 @@ def pip_install(
831
837
 
832
838
  """
833
839
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
834
- from meerschaum.config import get_config
835
840
  from meerschaum.config.static import STATIC_CONFIG
836
841
  from meerschaum.utils.warnings import warn
837
842
  if args is None:
838
843
  args = ['--upgrade'] if not _uninstall else []
839
- if color:
840
- ANSI, UNICODE = True, True
841
- else:
842
- ANSI, UNICODE = False, False
844
+ ANSI = True if color else False
843
845
  if check_wheel:
844
846
  have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
845
847
 
@@ -878,7 +880,8 @@ def pip_install(
878
880
  )
879
881
 
880
882
  use_uv_pip = (
881
- venv_contains_package('uv', venv=None, debug=debug)
883
+ _use_uv_pip
884
+ and venv_contains_package('uv', venv=None, debug=debug)
882
885
  and uv_bin is not None
883
886
  and venv is not None
884
887
  and is_uv_enabled()
@@ -966,6 +969,10 @@ def pip_install(
966
969
 
967
970
  if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
968
971
  if venv is not None:
972
+ vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
973
+ if not vtp.exists():
974
+ if not init_venv(venv, force=True):
975
+ vtp.mkdir(parents=True, exist_ok=True)
969
976
  _args += ['--target', venv_target_path(venv, debug=debug)]
970
977
  elif (
971
978
  '--target' not in _args
@@ -136,7 +136,7 @@ packages['sql'] = {
136
136
  'numpy' : 'numpy>=1.18.5',
137
137
  'pandas' : 'pandas[parquet]>=2.0.1',
138
138
  'pyarrow' : 'pyarrow>=16.1.0',
139
- 'dask' : 'dask[complete]>=2024.5.1',
139
+ 'dask' : 'dask[complete]>=2024.12.1',
140
140
  'partd' : 'partd>=1.4.2',
141
141
  'pytz' : 'pytz',
142
142
  'joblib' : 'joblib>=0.17.0',