meerschaum 2.2.0rc4__py3-none-any.whl → 2.2.2__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 (42) hide show
  1. meerschaum/_internal/entry.py +36 -11
  2. meerschaum/_internal/shell/Shell.py +40 -16
  3. meerschaum/_internal/term/__init__.py +3 -2
  4. meerschaum/_internal/term/tools.py +1 -1
  5. meerschaum/actions/api.py +65 -31
  6. meerschaum/actions/python.py +56 -24
  7. meerschaum/actions/start.py +2 -4
  8. meerschaum/actions/uninstall.py +5 -9
  9. meerschaum/actions/upgrade.py +11 -3
  10. meerschaum/api/__init__.py +1 -0
  11. meerschaum/api/dash/callbacks/__init__.py +4 -0
  12. meerschaum/api/dash/callbacks/custom.py +39 -0
  13. meerschaum/api/dash/callbacks/dashboard.py +39 -6
  14. meerschaum/api/dash/callbacks/login.py +3 -1
  15. meerschaum/api/dash/components.py +5 -2
  16. meerschaum/api/dash/pipes.py +145 -97
  17. meerschaum/config/_default.py +1 -0
  18. meerschaum/config/_paths.py +12 -12
  19. meerschaum/config/_version.py +1 -1
  20. meerschaum/config/paths.py +10 -0
  21. meerschaum/config/static/__init__.py +1 -1
  22. meerschaum/connectors/__init__.py +9 -2
  23. meerschaum/connectors/sql/_cli.py +7 -1
  24. meerschaum/connectors/sql/_pipes.py +6 -0
  25. meerschaum/core/Pipe/__init__.py +5 -0
  26. meerschaum/core/Pipe/_sync.py +2 -3
  27. meerschaum/plugins/__init__.py +67 -9
  28. meerschaum/utils/daemon/Daemon.py +7 -2
  29. meerschaum/utils/misc.py +6 -0
  30. meerschaum/utils/packages/__init__.py +212 -53
  31. meerschaum/utils/packages/_packages.py +3 -2
  32. meerschaum/utils/process.py +12 -2
  33. meerschaum/utils/schedule.py +10 -3
  34. meerschaum/utils/venv/__init__.py +46 -11
  35. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/METADATA +13 -9
  36. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/RECORD +42 -40
  37. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/WHEEL +1 -1
  38. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/LICENSE +0 -0
  39. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/NOTICE +0 -0
  40. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/entry_points.txt +0 -0
  41. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/top_level.txt +0 -0
  42. {meerschaum-2.2.0rc4.dist-info → meerschaum-2.2.2.dist-info}/zip-safe +0 -0
@@ -7,6 +7,7 @@ Expose plugin management APIs from the `meerschaum.plugins` module.
7
7
  """
8
8
 
9
9
  from __future__ import annotations
10
+ import functools
10
11
  from meerschaum.utils.typing import Callable, Any, Union, Optional, Dict, List, Tuple
11
12
  from meerschaum.utils.threading import Lock, RLock
12
13
  from meerschaum.plugins._Plugin import Plugin
@@ -16,6 +17,7 @@ _pre_sync_hooks: Dict[str, List[Callable[[Any], Any]]] = {}
16
17
  _post_sync_hooks: Dict[str, List[Callable[[Any], Any]]] = {}
17
18
  _locks = {
18
19
  '_api_plugins': RLock(),
20
+ '_dash_plugins': RLock(),
19
21
  '_pre_sync_hooks': RLock(),
20
22
  '_post_sync_hooks': RLock(),
21
23
  '__path__': RLock(),
@@ -156,6 +158,71 @@ def post_sync_hook(
156
158
  return function
157
159
 
158
160
 
161
+ _plugin_endpoints_to_pages = {}
162
+ def web_page(
163
+ page: Union[str, None, Callable[[Any], Any]] = None,
164
+ login_required: bool = True,
165
+ **kwargs
166
+ ) -> Any:
167
+ """
168
+ Quickly add pages to the dash application.
169
+
170
+ Examples
171
+ --------
172
+ >>> import meerschaum as mrsm
173
+ >>> from meerschaum.plugins import web_page
174
+ >>> html = mrsm.attempt_import('dash.html')
175
+ >>>
176
+ >>> @web_page('foo/bar', login_required=False)
177
+ >>> def foo_bar():
178
+ ... return html.Div([html.H1("Hello, World!")])
179
+ >>>
180
+ """
181
+ page_str = None
182
+
183
+ def _decorator(_func: Callable[[Any], Any]) -> Callable[[Any], Any]:
184
+ nonlocal page_str
185
+
186
+ @functools.wraps(_func)
187
+ def wrapper(*_args, **_kwargs):
188
+ return _func(*_args, **_kwargs)
189
+
190
+ if page_str is None:
191
+ page_str = _func.__name__
192
+
193
+ page_str = page_str.lstrip('/').rstrip('/').strip()
194
+ _plugin_endpoints_to_pages[page_str] = {
195
+ 'function': _func,
196
+ 'login_required': login_required,
197
+ }
198
+ return wrapper
199
+
200
+ if callable(page):
201
+ decorator_to_return = _decorator(page)
202
+ page_str = page.__name__
203
+ else:
204
+ decorator_to_return = _decorator
205
+ page_str = page
206
+
207
+ return decorator_to_return
208
+
209
+
210
+ _dash_plugins = {}
211
+ def dash_plugin(function: Callable[[Any], Any]) -> Callable[[Any], Any]:
212
+ """
213
+ Execute the function when starting the Dash application.
214
+ """
215
+ with _locks['_dash_plugins']:
216
+ try:
217
+ if function.__module__ not in _dash_plugins:
218
+ _dash_plugins[function.__module__] = []
219
+ _dash_plugins[function.__module__].append(function)
220
+ except Exception as e:
221
+ from meerschaum.utils.warnings import warn
222
+ warn(e)
223
+ return function
224
+
225
+
159
226
  def api_plugin(function: Callable[[Any], Any]) -> Callable[[Any], Any]:
160
227
  """
161
228
  Execute the function when initializing the Meerschaum API module.
@@ -164,15 +231,6 @@ def api_plugin(function: Callable[[Any], Any]) -> Callable[[Any], Any]:
164
231
 
165
232
  The FastAPI app will be passed as the only parameter.
166
233
 
167
- Parameters
168
- ----------
169
- function: Callable[[Any, Any]]
170
- The function to be called before starting the Meerschaum API.
171
-
172
- Returns
173
- -------
174
- Another function (decorator function).
175
-
176
234
  Examples
177
235
  --------
178
236
  >>> from meerschaum.plugins import api_plugin
@@ -465,8 +465,9 @@ class Daemon:
465
465
  Handle `SIGINT` within the Daemon context.
466
466
  This method is injected into the `DaemonContext`.
467
467
  """
468
- # from meerschaum.utils.daemon.FileDescriptorInterceptor import STOP_READING_FD_EVENT
469
- # STOP_READING_FD_EVENT.set()
468
+ from meerschaum.utils.process import signal_handler
469
+ signal_handler(signal_number, stack_frame)
470
+
470
471
  self.rotating_log.stop_log_fd_interception(unused_only=False)
471
472
  timer = self.__dict__.get('_log_refresh_timer', None)
472
473
  if timer is not None:
@@ -477,6 +478,7 @@ class Daemon:
477
478
  daemon_context.close()
478
479
 
479
480
  _close_pools()
481
+
480
482
  import threading
481
483
  for thread in threading.enumerate():
482
484
  if thread.name == 'MainThread':
@@ -495,6 +497,9 @@ class Daemon:
495
497
  Handle `SIGTERM` within the `Daemon` context.
496
498
  This method is injected into the `DaemonContext`.
497
499
  """
500
+ from meerschaum.utils.process import signal_handler
501
+ signal_handler(signal_number, stack_frame)
502
+
498
503
  timer = self.__dict__.get('_log_refresh_timer', None)
499
504
  if timer is not None:
500
505
  timer.cancel()
meerschaum/utils/misc.py CHANGED
@@ -1103,6 +1103,12 @@ def is_docker_available() -> bool:
1103
1103
  return has_docker
1104
1104
 
1105
1105
 
1106
+ def is_android() -> bool:
1107
+ """Return `True` if the current platform is Android."""
1108
+ import sys
1109
+ return hasattr(sys, 'getandroidapilevel')
1110
+
1111
+
1106
1112
  def is_bcp_available() -> bool:
1107
1113
  """Check if the MSSQL `bcp` utility is installed."""
1108
1114
  import subprocess
@@ -46,6 +46,7 @@ def get_module_path(
46
46
  """
47
47
  Get a module's path without importing.
48
48
  """
49
+ import site
49
50
  if debug:
50
51
  from meerschaum.utils.debug import dprint
51
52
  if not _try_install_name_on_fail:
@@ -54,33 +55,58 @@ def get_module_path(
54
55
  import_name_lower = install_name_lower
55
56
  else:
56
57
  import_name_lower = import_name.lower().replace('-', '_')
58
+
57
59
  vtp = venv_target_path(venv, allow_nonexistent=True, debug=debug)
58
60
  if not vtp.exists():
59
61
  if debug:
60
- dprint(f"Venv '{venv}' does not exist, cannot import '{import_name}'.", color=False)
62
+ dprint(
63
+ (
64
+ "Venv '{venv}' does not exist, cannot import "
65
+ + f"'{import_name}'."
66
+ ),
67
+ color = False,
68
+ )
61
69
  return None
70
+
71
+ venv_target_candidate_paths = [vtp]
72
+ if venv is None:
73
+ site_user_packages_dirs = [pathlib.Path(site.getusersitepackages())]
74
+ site_packages_dirs = [pathlib.Path(path) for path in site.getsitepackages()]
75
+
76
+ paths_to_add = [
77
+ path
78
+ for path in site_user_packages_dirs + site_packages_dirs
79
+ if path not in venv_target_candidate_paths
80
+ ]
81
+ venv_target_candidate_paths += paths_to_add
82
+
62
83
  candidates = []
63
- for file_name in os.listdir(vtp):
64
- file_name_lower = file_name.lower().replace('-', '_')
65
- if not file_name_lower.startswith(import_name_lower):
66
- continue
67
- if file_name.endswith('dist_info'):
84
+ for venv_target_candidate in venv_target_candidate_paths:
85
+ try:
86
+ file_names = os.listdir(venv_target_candidate)
87
+ except FileNotFoundError:
68
88
  continue
69
- file_path = vtp / file_name
89
+ for file_name in file_names:
90
+ file_name_lower = file_name.lower().replace('-', '_')
91
+ if not file_name_lower.startswith(import_name_lower):
92
+ continue
93
+ if file_name.endswith('dist_info'):
94
+ continue
95
+ file_path = venv_target_candidate / file_name
70
96
 
71
- ### Most likely: Is a directory with __init__.py
72
- if file_name_lower == import_name_lower and file_path.is_dir():
73
- init_path = file_path / '__init__.py'
74
- if init_path.exists():
75
- candidates.append(init_path)
97
+ ### Most likely: Is a directory with __init__.py
98
+ if file_name_lower == import_name_lower and file_path.is_dir():
99
+ init_path = file_path / '__init__.py'
100
+ if init_path.exists():
101
+ candidates.append(init_path)
76
102
 
77
- ### May be a standalone .py file.
78
- elif file_name_lower == import_name_lower + '.py':
79
- candidates.append(file_path)
103
+ ### May be a standalone .py file.
104
+ elif file_name_lower == import_name_lower + '.py':
105
+ candidates.append(file_path)
80
106
 
81
- ### Compiled wheels (e.g. pyodbc)
82
- elif file_name_lower.startswith(import_name_lower + '.'):
83
- candidates.append(file_path)
107
+ ### Compiled wheels (e.g. pyodbc)
108
+ elif file_name_lower.startswith(import_name_lower + '.'):
109
+ candidates.append(file_path)
84
110
 
85
111
  if len(candidates) == 1:
86
112
  return candidates[0]
@@ -466,12 +492,13 @@ def _get_package_metadata(import_name: str, venv: Optional[str]) -> Dict[str, st
466
492
  import re
467
493
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
468
494
  install_name = _import_to_install_name(import_name)
469
- _args = ['show', install_name]
495
+ _args = ['pip', 'show', install_name]
470
496
  if venv is not None:
471
497
  cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
472
- _args += ['--cache-dir', str(cache_dir_path)]
498
+ _args += ['--cache-dir', cache_dir_path.as_posix()]
499
+
473
500
  proc = run_python_package(
474
- 'pip', _args,
501
+ 'uv', _args,
475
502
  capture_output=True, as_proc=True, venv=venv, universal_newlines=True,
476
503
  )
477
504
  outs, errs = proc.communicate()
@@ -680,12 +707,22 @@ def need_update(
680
707
  return False
681
708
 
682
709
 
683
- def get_pip(venv: Optional[str] = 'mrsm', debug: bool=False) -> bool:
710
+ def get_pip(
711
+ venv: Optional[str] = 'mrsm',
712
+ color: bool = True,
713
+ debug: bool = False,
714
+ ) -> bool:
684
715
  """
685
716
  Download and run the get-pip.py script.
686
717
 
687
718
  Parameters
688
719
  ----------
720
+ venv: Optional[str], default 'mrsm'
721
+ The virtual environment into which to install `pip`.
722
+
723
+ color: bool, default True
724
+ If `True`, force color output.
725
+
689
726
  debug: bool, default False
690
727
  Verbosity toggle.
691
728
 
@@ -708,7 +745,7 @@ def get_pip(venv: Optional[str] = 'mrsm', debug: bool=False) -> bool:
708
745
  if venv is not None:
709
746
  init_venv(venv=venv, debug=debug)
710
747
  cmd_list = [venv_executable(venv=venv), dest.as_posix()]
711
- return subprocess.call(cmd_list, env=_get_pip_os_env()) == 0
748
+ return subprocess.call(cmd_list, env=_get_pip_os_env(color=color)) == 0
712
749
 
713
750
 
714
751
  def pip_install(
@@ -721,6 +758,8 @@ def pip_install(
721
758
  check_pypi: bool = True,
722
759
  check_wheel: bool = True,
723
760
  _uninstall: bool = False,
761
+ _from_completely_uninstall: bool = False,
762
+ _install_uv_pip: bool = True,
724
763
  color: bool = True,
725
764
  silent: bool = False,
726
765
  debug: bool = False,
@@ -776,7 +815,9 @@ def pip_install(
776
815
 
777
816
  """
778
817
  from meerschaum.config._paths import VIRTENV_RESOURCES_PATH
818
+ from meerschaum.config import get_config
779
819
  from meerschaum.utils.warnings import warn
820
+ from meerschaum.utils.misc import is_android
780
821
  if args is None:
781
822
  args = ['--upgrade'] if not _uninstall else []
782
823
  if color:
@@ -787,10 +828,43 @@ def pip_install(
787
828
  have_wheel = venv_contains_package('wheel', venv=venv, debug=debug)
788
829
 
789
830
  _args = list(args)
790
- have_pip = venv_contains_package('pip', venv=venv, debug=debug)
831
+ have_pip = venv_contains_package('pip', venv=None, debug=debug)
832
+ try:
833
+ import pip
834
+ have_pip = True
835
+ except ImportError:
836
+ have_pip = False
837
+ try:
838
+ import uv
839
+ uv_bin = uv.find_uv_bin()
840
+ have_uv_pip = True
841
+ except (ImportError, FileNotFoundError):
842
+ uv_bin = None
843
+ have_uv_pip = False
844
+ if have_pip and not have_uv_pip and _install_uv_pip and not is_android():
845
+ if not pip_install(
846
+ 'uv',
847
+ venv = None,
848
+ debug = debug,
849
+ _install_uv_pip = False,
850
+ check_update = False,
851
+ check_pypi = False,
852
+ check_wheel = False,
853
+ ):
854
+ warn(
855
+ f"Failed to install `uv` for virtual environment '{venv}'.",
856
+ color = False,
857
+ )
858
+
859
+ use_uv_pip = (
860
+ venv_contains_package('uv', venv=None, debug=debug)
861
+ and uv_bin is not None
862
+ and venv is not None
863
+ )
864
+
791
865
  import sys
792
- if not have_pip:
793
- if not get_pip(venv=venv, debug=debug):
866
+ if not have_pip and not use_uv_pip:
867
+ if not get_pip(venv=venv, color=color, debug=debug):
794
868
  import sys
795
869
  minor = sys.version_info.minor
796
870
  print(
@@ -806,13 +880,18 @@ def pip_install(
806
880
 
807
881
  with Venv(venv, debug=debug):
808
882
  if venv is not None:
809
- if '--ignore-installed' not in args and '-I' not in _args and not _uninstall:
883
+ if (
884
+ '--ignore-installed' not in args
885
+ and '-I' not in _args
886
+ and not _uninstall
887
+ and not use_uv_pip
888
+ ):
810
889
  _args += ['--ignore-installed']
811
890
  if '--cache-dir' not in args and not _uninstall:
812
891
  cache_dir_path = VIRTENV_RESOURCES_PATH / venv / 'cache'
813
892
  _args += ['--cache-dir', str(cache_dir_path)]
814
893
 
815
- if 'pip' not in ' '.join(_args):
894
+ if 'pip' not in ' '.join(_args) and not use_uv_pip:
816
895
  if check_update and not _uninstall:
817
896
  pip = attempt_import('pip', venv=venv, install=False, debug=debug, lazy=False)
818
897
  if need_update(pip, check_pypi=check_pypi, debug=debug):
@@ -820,17 +899,20 @@ def pip_install(
820
899
 
821
900
  _args = (['install'] if not _uninstall else ['uninstall']) + _args
822
901
 
823
- if check_wheel and not _uninstall:
902
+ if check_wheel and not _uninstall and not use_uv_pip:
824
903
  if not have_wheel:
825
904
  if not pip_install(
826
- 'setuptools', 'wheel',
905
+ 'setuptools', 'wheel', 'uv',
827
906
  venv = venv,
828
- check_update = False, check_pypi = False,
829
- check_wheel = False, debug = debug,
907
+ check_update = False,
908
+ check_pypi = False,
909
+ check_wheel = False,
910
+ debug = debug,
911
+ _install_uv_pip = False,
830
912
  ):
831
913
  warn(
832
914
  (
833
- "Failed to install `setuptools` and `wheel` for virtual "
915
+ "Failed to install `setuptools`, `wheel`, and `uv` for virtual "
834
916
  + f"environment '{venv}'."
835
917
  ),
836
918
  color = False,
@@ -838,24 +920,24 @@ def pip_install(
838
920
 
839
921
  if requirements_file_path is not None:
840
922
  _args.append('-r')
841
- _args.append(str(pathlib.Path(requirements_file_path).resolve()))
923
+ _args.append(pathlib.Path(requirements_file_path).resolve().as_posix())
842
924
 
843
925
  if not ANSI and '--no-color' not in _args:
844
926
  _args.append('--no-color')
845
927
 
846
- if '--no-input' not in _args:
928
+ if '--no-input' not in _args and not use_uv_pip:
847
929
  _args.append('--no-input')
848
930
 
849
- if _uninstall and '-y' not in _args:
931
+ if _uninstall and '-y' not in _args and not use_uv_pip:
850
932
  _args.append('-y')
851
933
 
852
- if '--no-warn-conflicts' not in _args and not _uninstall:
934
+ if '--no-warn-conflicts' not in _args and not _uninstall and not use_uv_pip:
853
935
  _args.append('--no-warn-conflicts')
854
936
 
855
- if '--disable-pip-version-check' not in _args:
937
+ if '--disable-pip-version-check' not in _args and not use_uv_pip:
856
938
  _args.append('--disable-pip-version-check')
857
939
 
858
- if '--target' not in _args and '-t' not in _args and not _uninstall:
940
+ if '--target' not in _args and '-t' not in _args and not (not use_uv_pip and _uninstall):
859
941
  if venv is not None:
860
942
  _args += ['--target', venv_target_path(venv, debug=debug)]
861
943
  elif (
@@ -863,12 +945,14 @@ def pip_install(
863
945
  and '-t' not in _args
864
946
  and not inside_venv()
865
947
  and not _uninstall
948
+ and not use_uv_pip
866
949
  ):
867
950
  _args += ['--user']
868
951
 
869
952
  if debug:
870
953
  if '-v' not in _args or '-vv' not in _args or '-vvv' not in _args:
871
- pass
954
+ if use_uv_pip:
955
+ _args.append('--verbose')
872
956
  else:
873
957
  if '-q' not in _args or '-qq' not in _args or '-qqq' not in _args:
874
958
  pass
@@ -883,10 +967,10 @@ def pip_install(
883
967
  if not silent:
884
968
  print(msg)
885
969
 
886
- if not _uninstall:
970
+ if _uninstall and not _from_completely_uninstall and not use_uv_pip:
887
971
  for install_name in _packages:
888
972
  _install_no_version = get_install_no_version(install_name)
889
- if _install_no_version in ('pip', 'wheel'):
973
+ if _install_no_version in ('pip', 'wheel', 'uv'):
890
974
  continue
891
975
  if not completely_uninstall_package(
892
976
  _install_no_version,
@@ -896,11 +980,17 @@ def pip_install(
896
980
  f"Failed to clean up package '{_install_no_version}'.",
897
981
  )
898
982
 
983
+ ### NOTE: Only append the `--prerelease=allow` flag if we explicitly depend on a prerelease.
984
+ if use_uv_pip:
985
+ _args.insert(0, 'pip')
986
+ if not _uninstall and get_prerelease_dependencies(_packages):
987
+ _args.append('--prerelease=allow')
988
+
899
989
  rc = run_python_package(
900
- 'pip',
990
+ ('pip' if not use_uv_pip else 'uv'),
901
991
  _args + _packages,
902
- venv = venv,
903
- env = _get_pip_os_env(),
992
+ venv = None,
993
+ env = _get_pip_os_env(color=color),
904
994
  debug = debug,
905
995
  )
906
996
  if debug:
@@ -918,6 +1008,33 @@ def pip_install(
918
1008
  return success
919
1009
 
920
1010
 
1011
+ def get_prerelease_dependencies(_packages: Optional[List[str]] = None):
1012
+ """
1013
+ Return a list of explicitly prerelease dependencies from a list of packages.
1014
+ """
1015
+ if _packages is None:
1016
+ _packages = list(all_packages.keys())
1017
+ prelrease_strings = ['dev', 'rc', 'a']
1018
+ prerelease_packages = []
1019
+ for install_name in _packages:
1020
+ _install_no_version = get_install_no_version(install_name)
1021
+ import_name = _install_to_import_name(install_name)
1022
+ install_with_version = _import_to_install_name(import_name)
1023
+ version_only = (
1024
+ install_with_version.lower().replace(_install_no_version.lower(), '')
1025
+ .split(']')[-1]
1026
+ )
1027
+
1028
+ is_prerelease = False
1029
+ for prelrease_string in prelrease_strings:
1030
+ if prelrease_string in version_only:
1031
+ is_prerelease = True
1032
+
1033
+ if is_prerelease:
1034
+ prerelease_packages.append(install_name)
1035
+ return prerelease_packages
1036
+
1037
+
921
1038
  def completely_uninstall_package(
922
1039
  install_name: str,
923
1040
  venv: str = 'mrsm',
@@ -944,7 +1061,7 @@ def completely_uninstall_package(
944
1061
  continue
945
1062
  installed_versions.append(file_name)
946
1063
 
947
- max_attempts = len(installed_versions) + 1
1064
+ max_attempts = len(installed_versions)
948
1065
  while attempts < max_attempts:
949
1066
  if not venv_contains_package(
950
1067
  _install_to_import_name(_install_no_version),
@@ -953,8 +1070,10 @@ def completely_uninstall_package(
953
1070
  return True
954
1071
  if not pip_uninstall(
955
1072
  _install_no_version,
956
- venv=venv,
957
- silent=(not debug), debug=debug
1073
+ venv = venv,
1074
+ silent = (not debug),
1075
+ _from_completely_uninstall = True,
1076
+ debug = debug,
958
1077
  ):
959
1078
  return False
960
1079
  attempts += 1
@@ -1031,6 +1150,10 @@ def run_python_package(
1031
1150
  if cwd is not None:
1032
1151
  os.chdir(cwd)
1033
1152
  executable = venv_executable(venv=venv)
1153
+ venv_path = (VIRTENV_RESOURCES_PATH / venv) if venv is not None else None
1154
+ env_dict = kw.get('env', os.environ).copy()
1155
+ if venv_path is not None:
1156
+ env_dict.update({'VIRTUAL_ENV': venv_path.as_posix()})
1034
1157
  command = [executable, '-m', str(package_name)] + [str(a) for a in args]
1035
1158
  import traceback
1036
1159
  if debug:
@@ -1055,7 +1178,7 @@ def run_python_package(
1055
1178
  command,
1056
1179
  stdout = stdout,
1057
1180
  stderr = stderr,
1058
- env = kw.get('env', os.environ),
1181
+ env = env_dict,
1059
1182
  )
1060
1183
  to_return = proc if as_proc else proc.wait()
1061
1184
  except KeyboardInterrupt:
@@ -1075,9 +1198,10 @@ def attempt_import(
1075
1198
  check_update: bool = False,
1076
1199
  check_pypi: bool = False,
1077
1200
  check_is_installed: bool = True,
1201
+ allow_outside_venv: bool = True,
1078
1202
  color: bool = True,
1079
1203
  debug: bool = False
1080
- ) -> Union[Any, Tuple[Any]]:
1204
+ ) -> Any:
1081
1205
  """
1082
1206
  Raise a warning if packages are not installed; otherwise import and return modules.
1083
1207
  If `lazy` is `True`, return lazy-imported modules.
@@ -1120,6 +1244,15 @@ def attempt_import(
1120
1244
  check_is_installed: bool, default True
1121
1245
  If `True`, check if the package is contained in the virtual environment.
1122
1246
 
1247
+ allow_outside_venv: bool, default True
1248
+ If `True`, search outside of the specified virtual environment
1249
+ if the package cannot be found.
1250
+ Setting to `False` will reinstall the package into a virtual environment, even if it
1251
+ is installed outside.
1252
+
1253
+ color: bool, default True
1254
+ If `False`, do not print ANSI colors.
1255
+
1123
1256
  Returns
1124
1257
  -------
1125
1258
  The specified modules. If they're not available and `install` is `True`, it will first
@@ -1201,6 +1334,7 @@ def attempt_import(
1201
1334
  name,
1202
1335
  venv = venv,
1203
1336
  split = split,
1337
+ allow_outside_venv = allow_outside_venv,
1204
1338
  debug = debug,
1205
1339
  )
1206
1340
  _is_installed_first_check[name] = package_is_installed
@@ -1346,7 +1480,9 @@ def import_rich(
1346
1480
  'pygments', lazy=False,
1347
1481
  )
1348
1482
  rich = attempt_import(
1349
- 'rich', lazy=lazy, **kw)
1483
+ 'rich', lazy=lazy,
1484
+ **kw
1485
+ )
1350
1486
  return rich
1351
1487
 
1352
1488
 
@@ -1580,10 +1716,26 @@ def is_installed(
1580
1716
  import_name: str,
1581
1717
  venv: Optional[str] = 'mrsm',
1582
1718
  split: bool = True,
1719
+ allow_outside_venv: bool = True,
1583
1720
  debug: bool = False,
1584
1721
  ) -> bool:
1585
1722
  """
1586
1723
  Check whether a package is installed.
1724
+
1725
+ Parameters
1726
+ ----------
1727
+ import_name: str
1728
+ The import name of the module.
1729
+
1730
+ venv: Optional[str], default 'mrsm'
1731
+ The venv in which to search for the module.
1732
+
1733
+ split: bool, default True
1734
+ If `True`, split on periods to determine the root module name.
1735
+
1736
+ allow_outside_venv: bool, default True
1737
+ If `True`, search outside of the specified virtual environment
1738
+ if the package cannot be found.
1587
1739
  """
1588
1740
  if debug:
1589
1741
  from meerschaum.utils.debug import dprint
@@ -1594,7 +1746,11 @@ def is_installed(
1594
1746
  spec_path = pathlib.Path(
1595
1747
  get_module_path(root_name, venv=venv, debug=debug)
1596
1748
  or
1597
- importlib.util.find_spec(root_name).origin
1749
+ (
1750
+ importlib.util.find_spec(root_name).origin
1751
+ if venv is not None and allow_outside_venv
1752
+ else None
1753
+ )
1598
1754
  )
1599
1755
  except (ModuleNotFoundError, ValueError, AttributeError, TypeError) as e:
1600
1756
  spec_path = None
@@ -1623,6 +1779,8 @@ def venv_contains_package(
1623
1779
  """
1624
1780
  Search the contents of a virtual environment for a package.
1625
1781
  """
1782
+ import site
1783
+ import pathlib
1626
1784
  root_name = import_name.split('.')[0] if split else import_name
1627
1785
  return get_module_path(root_name, venv=venv, debug=debug) is not None
1628
1786
 
@@ -1686,7 +1844,7 @@ def _monkey_patch_get_distribution(_dist: str, _version: str) -> None:
1686
1844
  pkg_resources.get_distribution = _get_distribution
1687
1845
 
1688
1846
 
1689
- def _get_pip_os_env():
1847
+ def _get_pip_os_env(color: bool = True):
1690
1848
  """
1691
1849
  Return the environment variables context in which `pip` should be run.
1692
1850
  See PEP 668 for why we are overriding the environment.
@@ -1695,5 +1853,6 @@ def _get_pip_os_env():
1695
1853
  pip_os_env = os.environ.copy()
1696
1854
  pip_os_env.update({
1697
1855
  'PIP_BREAK_SYSTEM_PACKAGES': 'true',
1856
+ ('FORCE_COLOR' if color else 'NO_COLOR'): '1',
1698
1857
  })
1699
1858
  return pip_os_env
@@ -53,6 +53,7 @@ packages: Dict[str, Dict[str, str]] = {
53
53
  'dill' : 'dill>=0.3.3',
54
54
  'virtualenv' : 'virtualenv>=20.1.0',
55
55
  'apscheduler' : 'APScheduler>=4.0.0a5',
56
+ 'uv' : 'uv>=0.2.11',
56
57
  },
57
58
  'drivers': {
58
59
  'cryptography' : 'cryptography>=38.0.1',
@@ -60,8 +61,8 @@ packages: Dict[str, Dict[str, str]] = {
60
61
  'pymysql' : 'PyMySQL>=0.9.0',
61
62
  'aiomysql' : 'aiomysql>=0.0.21',
62
63
  'sqlalchemy_cockroachdb' : 'sqlalchemy-cockroachdb>=2.0.0',
63
- 'duckdb' : 'duckdb<0.10.3',
64
- 'duckdb_engine' : 'duckdb-engine>=0.9.2',
64
+ 'duckdb' : 'duckdb>=1.0.0',
65
+ 'duckdb_engine' : 'duckdb-engine>=0.13.0',
65
66
  },
66
67
  'drivers-extras': {
67
68
  'pyodbc' : 'pyodbc>=4.0.30',