lionagi 0.10.7__py3-none-any.whl → 0.12.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 (39) hide show
  1. lionagi/adapters/__init__.py +1 -0
  2. lionagi/fields/file.py +1 -1
  3. lionagi/fields/reason.py +1 -1
  4. lionagi/libs/file/concat.py +6 -1
  5. lionagi/libs/file/concat_files.py +5 -1
  6. lionagi/libs/file/create_path.py +80 -0
  7. lionagi/libs/file/file_util.py +358 -0
  8. lionagi/libs/file/save.py +1 -1
  9. lionagi/libs/package/imports.py +177 -8
  10. lionagi/libs/parse/fuzzy_parse_json.py +117 -0
  11. lionagi/libs/parse/to_dict.py +336 -0
  12. lionagi/libs/parse/to_json.py +61 -0
  13. lionagi/libs/parse/to_num.py +378 -0
  14. lionagi/libs/parse/to_xml.py +57 -0
  15. lionagi/libs/parse/xml_parser.py +148 -0
  16. lionagi/libs/schema/breakdown_pydantic_annotation.py +48 -0
  17. lionagi/protocols/generic/log.py +2 -1
  18. lionagi/utils.py +123 -921
  19. lionagi/version.py +1 -1
  20. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/METADATA +8 -11
  21. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/RECORD +24 -30
  22. lionagi/libs/parse.py +0 -30
  23. lionagi/tools/browser/__init__.py +0 -0
  24. lionagi/tools/browser/providers/browser_use_.py +0 -3
  25. lionagi/tools/code/__init__.py +0 -3
  26. lionagi/tools/code/coder.py +0 -3
  27. lionagi/tools/code/manager.py +0 -3
  28. lionagi/tools/code/providers/__init__.py +0 -3
  29. lionagi/tools/code/providers/aider_.py +0 -3
  30. lionagi/tools/code/providers/e2b_.py +0 -3
  31. lionagi/tools/code/sandbox.py +0 -3
  32. lionagi/tools/file/manager.py +0 -3
  33. lionagi/tools/file/providers/__init__.py +0 -3
  34. lionagi/tools/file/providers/docling_.py +0 -3
  35. lionagi/tools/file/writer.py +0 -3
  36. lionagi/tools/query/__init__.py +0 -3
  37. /lionagi/{tools/browser/providers → libs/parse}/__init__.py +0 -0
  38. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/WHEEL +0 -0
  39. {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1 @@
1
+ """deprecated, will be removed in v0.13.0"""
lionagi/fields/file.py CHANGED
@@ -71,7 +71,7 @@ class File(HashableModel):
71
71
  header: str | None = None,
72
72
  footer: str | None = None,
73
73
  ) -> Path:
74
- from lionagi.utils import create_path
74
+ from lionagi.libs.file.create_path import create_path
75
75
 
76
76
  fp = create_path(
77
77
  directory=directory,
lionagi/fields/reason.py CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  from pydantic import Field, field_validator
6
6
 
7
+ from lionagi.libs.parse.to_num import to_num
7
8
  from lionagi.models import FieldModel, HashableModel
8
- from lionagi.utils import to_num
9
9
 
10
10
  __all__ = ("Reason",)
11
11
 
@@ -1,7 +1,12 @@
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
1
5
  from pathlib import Path
2
6
  from typing import Any
3
7
 
4
- from lionagi.utils import create_path, lcall
8
+ from lionagi.libs.file.create_path import create_path
9
+ from lionagi.utils import lcall
5
10
 
6
11
  from .process import dir_to_files
7
12
 
@@ -1,6 +1,10 @@
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
1
5
  from pathlib import Path
2
6
 
3
- from lionagi.utils import create_path
7
+ from lionagi.libs.file.create_path import create_path
4
8
 
5
9
  from .process import dir_to_files
6
10
 
@@ -0,0 +1,80 @@
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import uuid
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+
10
+ def create_path(
11
+ directory: Path | str,
12
+ filename: str,
13
+ extension: str = None,
14
+ timestamp: bool = False,
15
+ dir_exist_ok: bool = True,
16
+ file_exist_ok: bool = False,
17
+ time_prefix: bool = False,
18
+ timestamp_format: str | None = None,
19
+ random_hash_digits: int = 0,
20
+ ) -> Path:
21
+ """
22
+ Generate a new file path with optional timestamp and a random suffix.
23
+
24
+ Args:
25
+ directory: The directory where the file will be created.
26
+ filename: The base name of the file to create.
27
+ extension: The file extension, if not part of filename.
28
+ timestamp: If True, add a timestamp to the filename.
29
+ dir_exist_ok: If True, don't error if directory exists.
30
+ file_exist_ok: If True, allow overwriting existing files.
31
+ time_prefix: If True, timestamp is prefixed instead of suffixed.
32
+ timestamp_format: Custom format for timestamp (default: "%Y%m%d%H%M%S").
33
+ random_hash_digits: Number of hex digits for a random suffix.
34
+
35
+ Returns:
36
+ The full Path to the new or existing file.
37
+
38
+ Raises:
39
+ ValueError: If filename is invalid.
40
+ FileExistsError: If file exists and file_exist_ok=False.
41
+ """
42
+ if "/" in filename:
43
+ sub_dir, filename = filename.split("/")[:-1], filename.split("/")[-1]
44
+ directory = Path(directory) / "/".join(sub_dir)
45
+
46
+ if "\\" in filename:
47
+ raise ValueError("Filename cannot contain directory separators.")
48
+
49
+ directory = Path(directory)
50
+
51
+ # Extract name and extension from filename if present
52
+ if "." in filename:
53
+ name, ext = filename.rsplit(".", 1)
54
+ else:
55
+ name, ext = filename, extension
56
+
57
+ # Ensure extension has a single leading dot
58
+ ext = f".{ext.lstrip('.')}" if ext else ""
59
+
60
+ # Add timestamp if requested
61
+ if timestamp:
62
+ ts_str = datetime.now().strftime(timestamp_format or "%Y%m%d%H%M%S")
63
+ name = f"{ts_str}_{name}" if time_prefix else f"{name}_{ts_str}"
64
+
65
+ # Add random suffix if requested
66
+ if random_hash_digits > 0:
67
+ # Use UUID4 and truncate its hex for random suffix
68
+ random_suffix = uuid.uuid4().hex[:random_hash_digits]
69
+ name = f"{name}-{random_suffix}"
70
+
71
+ full_path = directory / f"{name}{ext}"
72
+
73
+ # Check if file or directory existence
74
+ full_path.parent.mkdir(parents=True, exist_ok=dir_exist_ok)
75
+ if full_path.exists() and not file_exist_ok:
76
+ raise FileExistsError(
77
+ f"File {full_path} already exists and file_exist_ok is False."
78
+ )
79
+
80
+ return full_path
@@ -0,0 +1,358 @@
1
+ # Copyright (c) 2023 - 2025, HaiyangLi <quantocean.li at gmail dot com>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from collections.abc import Callable
6
+ from pathlib import Path
7
+ from typing import Any, Literal
8
+
9
+
10
+ class FileUtil:
11
+
12
+ @staticmethod
13
+ def chunk_by_chars(
14
+ text: str,
15
+ chunk_size: int = 2048,
16
+ overlap: float = 0,
17
+ threshold: int = 256,
18
+ ) -> list[str]:
19
+ from .chunk import chunk_by_chars
20
+
21
+ return chunk_by_chars(
22
+ text,
23
+ chunk_size=chunk_size,
24
+ overlap=overlap,
25
+ threshold=threshold,
26
+ )
27
+
28
+ @staticmethod
29
+ def chunk_by_tokens(
30
+ text: str,
31
+ tokenizer: Callable[[str], list[str]] = str.split,
32
+ chunk_size: int = 1024,
33
+ overlap: float = 0,
34
+ threshold: int = 256,
35
+ ) -> list[str]:
36
+ from .chunk import chunk_by_tokens
37
+
38
+ return chunk_by_tokens(
39
+ text,
40
+ tokenizer=tokenizer,
41
+ chunk_size=chunk_size,
42
+ overlap=overlap,
43
+ threshold=threshold,
44
+ )
45
+
46
+ @staticmethod
47
+ def chunk_content(
48
+ content: str,
49
+ chunk_by: Literal["chars", "tokens"] = "chars",
50
+ tokenizer: Callable[[str], list[str]] = str.split,
51
+ chunk_size: int = 1024,
52
+ overlap: float = 0,
53
+ threshold: int = 256,
54
+ ) -> list[str]:
55
+ from .chunk import chunk_content
56
+
57
+ return chunk_content(
58
+ content,
59
+ chunk_by=chunk_by,
60
+ tokenizer=tokenizer,
61
+ chunk_size=chunk_size,
62
+ overlap=overlap,
63
+ threshold=threshold,
64
+ )
65
+
66
+ @staticmethod
67
+ def concat_files(
68
+ data_path: str | Path | list,
69
+ file_types: list[str],
70
+ output_dir: str | Path = None,
71
+ output_filename: str = None,
72
+ file_exist_ok: bool = True,
73
+ recursive: bool = True,
74
+ verbose: bool = True,
75
+ threshold: int = 0,
76
+ return_fps: bool = False,
77
+ return_files: bool = False,
78
+ **kwargs,
79
+ ) -> (
80
+ list[str] | str | tuple[list[str], list[Path]] | tuple[str, list[Path]]
81
+ ):
82
+ from .concat_files import concat_files
83
+
84
+ return concat_files(
85
+ data_path,
86
+ file_types=file_types,
87
+ output_dir=output_dir,
88
+ output_filename=output_filename,
89
+ file_exist_ok=file_exist_ok,
90
+ recursive=recursive,
91
+ verbose=verbose,
92
+ threshold=threshold,
93
+ return_fps=return_fps,
94
+ return_files=return_files,
95
+ **kwargs,
96
+ )
97
+
98
+ @staticmethod
99
+ def concat(
100
+ data_path: str | Path | list,
101
+ file_types: list[str],
102
+ output_dir: str | Path = None,
103
+ output_filename: str = None,
104
+ file_exist_ok: bool = True,
105
+ recursive: bool = True,
106
+ verbose: bool = True,
107
+ threshold: int = 0,
108
+ return_fps: bool = False,
109
+ return_files: bool = False,
110
+ exclude_patterns: list[str] = None,
111
+ **kwargs,
112
+ ) -> dict[str, Any]:
113
+ from .concat import concat
114
+
115
+ return concat(
116
+ data_path,
117
+ file_types=file_types,
118
+ output_dir=output_dir,
119
+ output_filename=output_filename,
120
+ file_exist_ok=file_exist_ok,
121
+ recursive=recursive,
122
+ verbose=verbose,
123
+ threshold=threshold,
124
+ return_fps=return_fps,
125
+ return_files=return_files,
126
+ exclude_patterns=exclude_patterns,
127
+ **kwargs,
128
+ )
129
+
130
+ @staticmethod
131
+ def copy_file(src: Path | str, dest: Path | str) -> None:
132
+ from .file_ops import copy_file
133
+
134
+ copy_file(src, dest)
135
+
136
+ @staticmethod
137
+ def get_file_size(path: Path | str) -> int:
138
+ from .file_ops import get_file_size
139
+
140
+ return get_file_size(path)
141
+
142
+ @staticmethod
143
+ def list_files(
144
+ dir_path: Path | str, extension: str | None = None
145
+ ) -> list[Path]:
146
+ from .file_ops import list_files
147
+
148
+ return list_files(dir_path, extension=extension)
149
+
150
+ @staticmethod
151
+ def read_file(file_path: Path | str, encoding: str = "utf-8") -> str:
152
+ from .file_ops import read_file
153
+
154
+ return read_file(file_path, encoding=encoding)
155
+
156
+ @staticmethod
157
+ def read_image_to_base64(image_path: str | Path) -> str:
158
+ import base64
159
+
160
+ import cv2 # type: ignore[import]
161
+
162
+ image_path = str(image_path)
163
+ image = cv2.imread(image_path, cv2.COLOR_BGR2RGB)
164
+
165
+ if image is None:
166
+ raise ValueError(f"Could not read image from path: {image_path}")
167
+
168
+ file_extension = "." + image_path.split(".")[-1]
169
+
170
+ success, buffer = cv2.imencode(file_extension, image)
171
+ if not success:
172
+ raise ValueError(
173
+ f"Could not encode image to {file_extension} format."
174
+ )
175
+ encoded_image = base64.b64encode(buffer).decode("utf-8")
176
+ return encoded_image
177
+
178
+ @staticmethod
179
+ def pdf_to_images(
180
+ pdf_path: str, output_folder: str, dpi: int = 300, fmt: str = "jpeg"
181
+ ) -> list:
182
+ """
183
+ Convert a PDF file into images, one image per page.
184
+
185
+ Args:
186
+ pdf_path (str): Path to the input PDF file.
187
+ output_folder (str): Directory to save the output images.
188
+ dpi (int): Dots per inch (resolution) for conversion (default: 300).
189
+ fmt (str): Image format (default: 'jpeg'). Use 'png' if preferred.
190
+
191
+ Returns:
192
+ list: A list of file paths for the saved images.
193
+ """
194
+ import os
195
+
196
+ from lionagi.utils import check_import
197
+
198
+ convert_from_path = check_import(
199
+ "pdf2image", import_name="convert_from_path"
200
+ )
201
+
202
+ # Ensure the output folder exists
203
+ os.makedirs(output_folder, exist_ok=True)
204
+
205
+ # Convert PDF to a list of PIL Image objects
206
+ images = convert_from_path(pdf_path, dpi=dpi)
207
+
208
+ saved_paths = []
209
+ for i, image in enumerate(images):
210
+ # Construct the output file name
211
+ image_file = os.path.join(output_folder, f"page_{i + 1}.{fmt}")
212
+ image.save(image_file, fmt.upper())
213
+ saved_paths.append(image_file)
214
+
215
+ return saved_paths
216
+
217
+ @staticmethod
218
+ def dir_to_files(
219
+ directory: str | Path,
220
+ file_types: list[str] | None = None,
221
+ max_workers: int | None = None,
222
+ ignore_errors: bool = False,
223
+ verbose: bool = False,
224
+ recursive: bool = False,
225
+ ) -> list[Path]:
226
+ from .process import dir_to_files
227
+
228
+ return dir_to_files(
229
+ directory,
230
+ file_types=file_types,
231
+ max_workers=max_workers,
232
+ ignore_errors=ignore_errors,
233
+ verbose=verbose,
234
+ recursive=recursive,
235
+ )
236
+
237
+ @staticmethod
238
+ def file_to_chunks(
239
+ file_path: str | Path,
240
+ chunk_by: Literal["chars", "tokens"] = "chars",
241
+ chunk_size: int = 1500,
242
+ overlap: float = 0.1,
243
+ threshold: int = 200,
244
+ encoding: str = "utf-8",
245
+ custom_metadata: dict[str, Any] | None = None,
246
+ output_dir: str | Path | None = None,
247
+ verbose: bool = False,
248
+ timestamp: bool = True,
249
+ random_hash_digits: int = 4,
250
+ as_node: bool = False,
251
+ ) -> list[dict[str, Any]]:
252
+ from .file_ops import file_to_chunks
253
+
254
+ return file_to_chunks(
255
+ file_path,
256
+ chunk_by=chunk_by,
257
+ chunk_size=chunk_size,
258
+ overlap=overlap,
259
+ threshold=threshold,
260
+ encoding=encoding,
261
+ custom_metadata=custom_metadata,
262
+ output_dir=output_dir,
263
+ verbose=verbose,
264
+ timestamp=timestamp,
265
+ random_hash_digits=random_hash_digits,
266
+ as_node=as_node,
267
+ )
268
+
269
+ @staticmethod
270
+ def chunk(
271
+ *,
272
+ text: str | None = None,
273
+ url_or_path: str | Path = None,
274
+ file_types: list[str] | None = None, # only local files
275
+ recursive: bool = False, # only local files
276
+ tokenizer: Callable[[str], list[str]] = None,
277
+ chunk_by: Literal["chars", "tokens"] = "chars",
278
+ chunk_size: int = 1500,
279
+ overlap: float = 0.1,
280
+ threshold: int = 200,
281
+ output_file: str | Path | None = None,
282
+ metadata: dict[str, Any] | None = None,
283
+ reader_tool: Callable = None,
284
+ as_node: bool = False,
285
+ ) -> list:
286
+ from .process import chunk
287
+
288
+ return chunk(
289
+ text=text,
290
+ url_or_path=url_or_path,
291
+ file_types=file_types,
292
+ recursive=recursive,
293
+ tokenizer=tokenizer,
294
+ chunk_by=chunk_by,
295
+ chunk_size=chunk_size,
296
+ overlap=overlap,
297
+ threshold=threshold,
298
+ output_file=output_file,
299
+ metadata=metadata,
300
+ reader_tool=reader_tool,
301
+ as_node=as_node,
302
+ )
303
+
304
+ @staticmethod
305
+ def save_to_file(
306
+ text: str,
307
+ directory: str | Path,
308
+ filename: str,
309
+ extension: str = "txt",
310
+ timestamp: bool = True,
311
+ dir_exist_ok: bool = True,
312
+ file_exist_ok: bool = True,
313
+ time_prefix: bool = False,
314
+ timestamp_format: str = "%Y%m%d_%H%M%S",
315
+ random_hash_digits: int = 4,
316
+ verbose: bool = False,
317
+ ) -> Path:
318
+ from .save import save_to_file
319
+
320
+ return save_to_file(
321
+ text,
322
+ directory=directory,
323
+ filename=filename,
324
+ extension=extension,
325
+ timestamp=timestamp,
326
+ dir_exist_ok=dir_exist_ok,
327
+ file_exist_ok=file_exist_ok,
328
+ time_prefix=time_prefix,
329
+ timestamp_format=timestamp_format,
330
+ random_hash_digits=random_hash_digits,
331
+ verbose=verbose,
332
+ )
333
+
334
+ @staticmethod
335
+ def create_path(
336
+ directory: Path | str,
337
+ filename: str,
338
+ extension: str = None,
339
+ timestamp: bool = False,
340
+ dir_exist_ok: bool = True,
341
+ file_exist_ok: bool = False,
342
+ time_prefix: bool = False,
343
+ timestamp_format: str | None = None,
344
+ random_hash_digits: int = 0,
345
+ ) -> Path:
346
+ from .create_path import create_path
347
+
348
+ return create_path(
349
+ directory=directory,
350
+ filename=filename,
351
+ extension=extension,
352
+ timestamp=timestamp,
353
+ dir_exist_ok=dir_exist_ok,
354
+ file_exist_ok=file_exist_ok,
355
+ time_prefix=time_prefix,
356
+ timestamp_format=timestamp_format,
357
+ random_hash_digits=random_hash_digits,
358
+ )
lionagi/libs/file/save.py CHANGED
@@ -7,7 +7,7 @@ import logging
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
- from lionagi.utils import create_path
10
+ from .create_path import create_path
11
11
 
12
12
 
13
13
  def save_to_file(
@@ -2,15 +2,184 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
- from lionagi.utils import (
6
- check_import,
7
- import_module,
8
- install_import,
9
- is_import_installed,
10
- run_package_manager_command,
11
- )
5
+ import logging
6
+ import shutil
7
+ import subprocess
8
+ import sys
9
+ from collections.abc import Sequence
10
+ from typing import Any
11
+
12
+ from lionagi.utils import is_import_installed
13
+
14
+
15
+ def run_package_manager_command(
16
+ args: Sequence[str],
17
+ ) -> subprocess.CompletedProcess[bytes]:
18
+ """Run a package manager command, using uv if available, otherwise falling back to pip."""
19
+ # Check if uv is available in PATH
20
+ uv_path = shutil.which("uv")
21
+
22
+ if uv_path:
23
+ # Use uv if available
24
+ try:
25
+ return subprocess.run(
26
+ [uv_path] + list(args),
27
+ check=True,
28
+ capture_output=True,
29
+ )
30
+ except subprocess.CalledProcessError:
31
+ # If uv fails, fall back to pip
32
+ print("uv command failed, falling back to pip...")
33
+
34
+ # Fall back to pip
35
+ return subprocess.run(
36
+ [sys.executable, "-m", "pip"] + list(args),
37
+ check=True,
38
+ capture_output=True,
39
+ )
40
+
41
+
42
+ def check_import(
43
+ package_name: str,
44
+ module_name: str | None = None,
45
+ import_name: str | None = None,
46
+ pip_name: str | None = None,
47
+ attempt_install: bool = True,
48
+ error_message: str = "",
49
+ ):
50
+ """
51
+ Check if a package is installed, attempt to install if not.
52
+
53
+ Args:
54
+ package_name: The name of the package to check.
55
+ module_name: The specific module to import (if any).
56
+ import_name: The specific name to import from the module (if any).
57
+ pip_name: The name to use for pip installation (if different).
58
+ attempt_install: Whether to attempt installation if not found.
59
+ error_message: Custom error message to use if package not found.
60
+
61
+ Raises:
62
+ ImportError: If the package is not found and not installed.
63
+ ValueError: If the import fails after installation attempt.
64
+ """
65
+ if not is_import_installed(package_name):
66
+ if attempt_install:
67
+ logging.info(
68
+ f"Package {package_name} not found. Attempting " "to install.",
69
+ )
70
+ try:
71
+ return install_import(
72
+ package_name=package_name,
73
+ module_name=module_name,
74
+ import_name=import_name,
75
+ pip_name=pip_name,
76
+ )
77
+ except ImportError as e:
78
+ raise ValueError(
79
+ f"Failed to install {package_name}: {e}"
80
+ ) from e
81
+ else:
82
+ logging.info(
83
+ f"Package {package_name} not found. {error_message}",
84
+ )
85
+ raise ImportError(
86
+ f"Package {package_name} not found. {error_message}",
87
+ )
88
+
89
+ return import_module(
90
+ package_name=package_name,
91
+ module_name=module_name,
92
+ import_name=import_name,
93
+ )
94
+
95
+
96
+ def import_module(
97
+ package_name: str,
98
+ module_name: str = None,
99
+ import_name: str | list = None,
100
+ ) -> Any:
101
+ """
102
+ Import a module by its path.
103
+
104
+ Args:
105
+ module_path: The path of the module to import.
106
+
107
+ Returns:
108
+ The imported module.
109
+
110
+ Raises:
111
+ ImportError: If the module cannot be imported.
112
+ """
113
+ try:
114
+ full_import_path = (
115
+ f"{package_name}.{module_name}" if module_name else package_name
116
+ )
117
+
118
+ if import_name:
119
+ import_name = (
120
+ [import_name]
121
+ if not isinstance(import_name, list)
122
+ else import_name
123
+ )
124
+ a = __import__(
125
+ full_import_path,
126
+ fromlist=import_name,
127
+ )
128
+ if len(import_name) == 1:
129
+ return getattr(a, import_name[0])
130
+ return [getattr(a, name) for name in import_name]
131
+ else:
132
+ return __import__(full_import_path)
133
+
134
+ except ImportError as e:
135
+ raise ImportError(
136
+ f"Failed to import module {full_import_path}: {e}"
137
+ ) from e
138
+
139
+
140
+ def install_import(
141
+ package_name: str,
142
+ module_name: str | None = None,
143
+ import_name: str | None = None,
144
+ pip_name: str | None = None,
145
+ ):
146
+ """
147
+ Attempt to import a package, installing it if not found.
148
+
149
+ Args:
150
+ package_name: The name of the package to import.
151
+ module_name: The specific module to import (if any).
152
+ import_name: The specific name to import from the module (if any).
153
+ pip_name: The name to use for pip installation (if different).
154
+
155
+ Raises:
156
+ ImportError: If the package cannot be imported or installed.
157
+ subprocess.CalledProcessError: If pip installation fails.
158
+ """
159
+ pip_name = pip_name or package_name
160
+
161
+ try:
162
+ return import_module(
163
+ package_name=package_name,
164
+ module_name=module_name,
165
+ import_name=import_name,
166
+ )
167
+ except ImportError:
168
+ logging.info(f"Installing {pip_name}...")
169
+ try:
170
+ run_package_manager_command(["install", pip_name])
171
+ return import_module(
172
+ package_name=package_name,
173
+ module_name=module_name,
174
+ import_name=import_name,
175
+ )
176
+ except subprocess.CalledProcessError as e:
177
+ raise ImportError(f"Failed to install {pip_name}: {e}") from e
178
+ except ImportError as e:
179
+ raise ImportError(
180
+ f"Failed to import {pip_name} after installation: {e}"
181
+ ) from e
12
182
 
13
- # backward compatibility
14
183
 
15
184
  __all__ = (
16
185
  "run_package_manager_command",