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.
- lionagi/adapters/__init__.py +1 -0
- lionagi/fields/file.py +1 -1
- lionagi/fields/reason.py +1 -1
- lionagi/libs/file/concat.py +6 -1
- lionagi/libs/file/concat_files.py +5 -1
- lionagi/libs/file/create_path.py +80 -0
- lionagi/libs/file/file_util.py +358 -0
- lionagi/libs/file/save.py +1 -1
- lionagi/libs/package/imports.py +177 -8
- lionagi/libs/parse/fuzzy_parse_json.py +117 -0
- lionagi/libs/parse/to_dict.py +336 -0
- lionagi/libs/parse/to_json.py +61 -0
- lionagi/libs/parse/to_num.py +378 -0
- lionagi/libs/parse/to_xml.py +57 -0
- lionagi/libs/parse/xml_parser.py +148 -0
- lionagi/libs/schema/breakdown_pydantic_annotation.py +48 -0
- lionagi/protocols/generic/log.py +2 -1
- lionagi/utils.py +123 -921
- lionagi/version.py +1 -1
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/METADATA +8 -11
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/RECORD +24 -30
- lionagi/libs/parse.py +0 -30
- lionagi/tools/browser/__init__.py +0 -0
- lionagi/tools/browser/providers/browser_use_.py +0 -3
- lionagi/tools/code/__init__.py +0 -3
- lionagi/tools/code/coder.py +0 -3
- lionagi/tools/code/manager.py +0 -3
- lionagi/tools/code/providers/__init__.py +0 -3
- lionagi/tools/code/providers/aider_.py +0 -3
- lionagi/tools/code/providers/e2b_.py +0 -3
- lionagi/tools/code/sandbox.py +0 -3
- lionagi/tools/file/manager.py +0 -3
- lionagi/tools/file/providers/__init__.py +0 -3
- lionagi/tools/file/providers/docling_.py +0 -3
- lionagi/tools/file/writer.py +0 -3
- lionagi/tools/query/__init__.py +0 -3
- /lionagi/{tools/browser/providers → libs/parse}/__init__.py +0 -0
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/WHEEL +0 -0
- {lionagi-0.10.7.dist-info → lionagi-0.12.0.dist-info}/licenses/LICENSE +0 -0
lionagi/adapters/__init__.py
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
"""deprecated, will be removed in v0.13.0"""
|
lionagi/fields/file.py
CHANGED
lionagi/fields/reason.py
CHANGED
lionagi/libs/file/concat.py
CHANGED
@@ -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.
|
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.
|
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
lionagi/libs/package/imports.py
CHANGED
@@ -2,15 +2,184 @@
|
|
2
2
|
#
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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",
|