waldiez 0.4.8__py3-none-any.whl → 0.4.11__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.
Potentially problematic release.
This version of waldiez might be problematic. Click here for more details.
- waldiez/__init__.py +1 -2
- waldiez/_version.py +1 -1
- waldiez/cli.py +90 -96
- waldiez/exporter.py +64 -9
- waldiez/exporting/agent/extras/rag/chroma_extras.py +21 -9
- waldiez/exporting/core/context.py +12 -0
- waldiez/exporting/core/extras/flow_extras.py +2 -14
- waldiez/exporting/core/types.py +21 -0
- waldiez/exporting/flow/exporter.py +4 -0
- waldiez/exporting/flow/factory.py +16 -0
- waldiez/exporting/flow/orchestrator.py +12 -0
- waldiez/exporting/flow/utils/__init__.py +2 -0
- waldiez/exporting/flow/utils/common.py +96 -2
- waldiez/exporting/flow/utils/logging.py +5 -6
- waldiez/io/mqtt.py +7 -3
- waldiez/io/structured.py +26 -2
- waldiez/models/common/method_utils.py +1 -1
- waldiez/models/tool/tool.py +2 -1
- waldiez/runner.py +402 -332
- waldiez/running/__init__.py +6 -28
- waldiez/running/base_runner.py +907 -0
- waldiez/running/environment.py +74 -0
- waldiez/running/import_runner.py +424 -0
- waldiez/running/patch_io_stream.py +208 -0
- waldiez/running/post_run.py +121 -0
- waldiez/running/pre_run.py +105 -0
- waldiez/running/protocol.py +281 -0
- waldiez/running/run_results.py +22 -0
- waldiez/running/subprocess_runner.py +100 -0
- waldiez/running/utils.py +134 -0
- waldiez/utils/__init__.py +2 -2
- waldiez/utils/version.py +49 -0
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/METADATA +11 -11
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/RECORD +38 -30
- waldiez/running/running.py +0 -388
- waldiez/utils/flaml_warnings.py +0 -17
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/WHEEL +0 -0
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/entry_points.txt +0 -0
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.4.8.dist-info → waldiez-0.4.11.dist-info}/licenses/NOTICE.md +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
# pylint: disable=import-outside-toplevel, broad-exception-caught
|
|
5
|
+
# pylint: disable=too-many-try-statements, too-complex
|
|
6
|
+
# noqa: C901
|
|
7
|
+
"""Patch ag2's IOStream if a flow is async.
|
|
8
|
+
|
|
9
|
+
# let's keep an eye here:
|
|
10
|
+
# https://github.com/ag2ai/ag2/blob/main/autogen/agentchat/conversable_agent.py#L2973
|
|
11
|
+
# reply = await iostream.input(prompt) ???? (await???)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import inspect
|
|
15
|
+
import sys
|
|
16
|
+
from typing import Any, Callable
|
|
17
|
+
|
|
18
|
+
from asyncer import syncify
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def patch_io_stream(is_async: bool) -> None:
|
|
22
|
+
"""Patch the IOStream to handle async flows.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
is_async : bool
|
|
27
|
+
Whether the flow is async or not.
|
|
28
|
+
"""
|
|
29
|
+
if is_async:
|
|
30
|
+
patch_async_io_stream()
|
|
31
|
+
else:
|
|
32
|
+
patch_sync_io_stream()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def patch_sync_io_stream() -> None:
|
|
36
|
+
"""Patch the IOStream to handle async flows."""
|
|
37
|
+
from autogen.io import IOStream # type: ignore
|
|
38
|
+
|
|
39
|
+
iostream = IOStream.get_default()
|
|
40
|
+
original_input = iostream.input
|
|
41
|
+
|
|
42
|
+
def _safe_input(prompt: str = "", *, password: bool = False) -> str:
|
|
43
|
+
"""Async input method."""
|
|
44
|
+
try:
|
|
45
|
+
input_or_coro = original_input(prompt, password=password)
|
|
46
|
+
if inspect.iscoroutine(input_or_coro):
|
|
47
|
+
|
|
48
|
+
async def _async_input() -> str:
|
|
49
|
+
reply = await input_or_coro
|
|
50
|
+
return reply
|
|
51
|
+
|
|
52
|
+
return syncify(_async_input)()
|
|
53
|
+
return input_or_coro
|
|
54
|
+
|
|
55
|
+
except EOFError:
|
|
56
|
+
# Handle EOFError gracefully
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
iostream.input = _safe_input # pyright: ignore
|
|
60
|
+
iostream.print = get_printer() # pyright: ignore
|
|
61
|
+
IOStream.set_default(iostream)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def patch_async_io_stream() -> None:
|
|
65
|
+
"""Patch the IOStream to handle async flows."""
|
|
66
|
+
from autogen.io import IOStream # pyright: ignore
|
|
67
|
+
|
|
68
|
+
iostream = IOStream.get_default()
|
|
69
|
+
original_input = iostream.input
|
|
70
|
+
|
|
71
|
+
async def _async_input(prompt: str = "", *, password: bool = False) -> str:
|
|
72
|
+
"""Async input method."""
|
|
73
|
+
try:
|
|
74
|
+
input_or_coro = original_input(prompt, password=password)
|
|
75
|
+
if inspect.iscoroutine(input_or_coro):
|
|
76
|
+
reply = await input_or_coro
|
|
77
|
+
else:
|
|
78
|
+
reply = input_or_coro
|
|
79
|
+
return reply
|
|
80
|
+
except EOFError:
|
|
81
|
+
# Handle EOFError gracefully
|
|
82
|
+
return ""
|
|
83
|
+
|
|
84
|
+
iostream.input = _async_input # pyright: ignore
|
|
85
|
+
iostream.print = get_printer() # pyright: ignore
|
|
86
|
+
IOStream.set_default(iostream)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_printer() -> Callable[..., None]: # noqa: C901
|
|
90
|
+
"""Get the printer function.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
Callable[..., None]
|
|
95
|
+
The printer function that handles Unicode encoding errors gracefully.
|
|
96
|
+
"""
|
|
97
|
+
try:
|
|
98
|
+
from autogen.io import IOStream # pyright: ignore
|
|
99
|
+
|
|
100
|
+
printer = IOStream.get_default().print
|
|
101
|
+
except ImportError:
|
|
102
|
+
# Fallback to standard print if autogen is not available
|
|
103
|
+
printer = print
|
|
104
|
+
|
|
105
|
+
def safe_printer(*args: Any, **kwargs: Any) -> None: # noqa: C901
|
|
106
|
+
"""Safe printer that handles Unicode encoding errors.
|
|
107
|
+
|
|
108
|
+
Parameters
|
|
109
|
+
----------
|
|
110
|
+
*args : Any
|
|
111
|
+
Arguments to pass to the printer
|
|
112
|
+
**kwargs : Any
|
|
113
|
+
Keyword arguments to pass to the printer
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
printer(*args, **kwargs)
|
|
117
|
+
except UnicodeEncodeError:
|
|
118
|
+
# First fallback: try to get a safe string representation
|
|
119
|
+
try:
|
|
120
|
+
msg, flush = get_what_to_print(*args, **kwargs)
|
|
121
|
+
# Convert problematic characters to safe representations
|
|
122
|
+
safe_msg = msg.encode("utf-8", errors="replace").decode("utf-8")
|
|
123
|
+
printer(safe_msg, end="", flush=flush)
|
|
124
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
125
|
+
# Second fallback: use built-in print with safe encoding
|
|
126
|
+
try:
|
|
127
|
+
# Convert args to safe string representations
|
|
128
|
+
safe_args: list[str] = []
|
|
129
|
+
for arg in args:
|
|
130
|
+
try:
|
|
131
|
+
safe_args.append(
|
|
132
|
+
str(arg)
|
|
133
|
+
.encode("utf-8", errors="replace")
|
|
134
|
+
.decode("utf-8")
|
|
135
|
+
)
|
|
136
|
+
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
137
|
+
safe_args.append(repr(arg))
|
|
138
|
+
|
|
139
|
+
# Use built-in print instead of the custom printer
|
|
140
|
+
print(*safe_args, **kwargs)
|
|
141
|
+
|
|
142
|
+
except Exception:
|
|
143
|
+
# Final fallback: write directly to stderr buffer
|
|
144
|
+
try:
|
|
145
|
+
error_msg = (
|
|
146
|
+
"Could not print the message "
|
|
147
|
+
"due to encoding issues.\n"
|
|
148
|
+
)
|
|
149
|
+
if hasattr(sys.stderr, "buffer"):
|
|
150
|
+
sys.stderr.buffer.write(
|
|
151
|
+
error_msg.encode("utf-8", errors="replace")
|
|
152
|
+
)
|
|
153
|
+
sys.stderr.buffer.flush()
|
|
154
|
+
else:
|
|
155
|
+
sys.stderr.write(error_msg)
|
|
156
|
+
sys.stderr.flush()
|
|
157
|
+
except Exception:
|
|
158
|
+
# If even this fails, we're in a very bad state
|
|
159
|
+
pass
|
|
160
|
+
except Exception as e:
|
|
161
|
+
# Handle any other unexpected errors
|
|
162
|
+
try:
|
|
163
|
+
error_msg = f"Unexpected error in printer: {str(e)}\n"
|
|
164
|
+
if hasattr(sys.stderr, "buffer"):
|
|
165
|
+
sys.stderr.buffer.write(
|
|
166
|
+
error_msg.encode("utf-8", errors="replace")
|
|
167
|
+
)
|
|
168
|
+
sys.stderr.buffer.flush()
|
|
169
|
+
else:
|
|
170
|
+
sys.stderr.write(error_msg)
|
|
171
|
+
sys.stderr.flush()
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
return safe_printer
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_what_to_print(*args: Any, **kwargs: Any) -> tuple[str, bool]:
|
|
179
|
+
"""Extract message and flush flag from print arguments.
|
|
180
|
+
|
|
181
|
+
Parameters
|
|
182
|
+
----------
|
|
183
|
+
*args : Any
|
|
184
|
+
Arguments to print
|
|
185
|
+
**kwargs : Any
|
|
186
|
+
Keyword arguments for print function
|
|
187
|
+
|
|
188
|
+
Returns
|
|
189
|
+
-------
|
|
190
|
+
tuple[str, bool]
|
|
191
|
+
Message to print and flush flag
|
|
192
|
+
"""
|
|
193
|
+
# Convert all args to strings and join with spaces (like print does)
|
|
194
|
+
msg = " ".join(str(arg) for arg in args)
|
|
195
|
+
|
|
196
|
+
# Handle sep parameter
|
|
197
|
+
sep = kwargs.get("sep", " ")
|
|
198
|
+
if len(args) > 1:
|
|
199
|
+
msg = sep.join(str(arg) for arg in args)
|
|
200
|
+
|
|
201
|
+
# Handle end parameter
|
|
202
|
+
end = kwargs.get("end", "\n")
|
|
203
|
+
msg += end
|
|
204
|
+
|
|
205
|
+
# Handle flush parameter
|
|
206
|
+
flush = kwargs.get("flush", False)
|
|
207
|
+
|
|
208
|
+
return msg, flush
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
|
|
4
|
+
# pylint: disable=unused-argument
|
|
5
|
+
"""Utilities for running code."""
|
|
6
|
+
|
|
7
|
+
import datetime
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional, Union
|
|
11
|
+
|
|
12
|
+
from .gen_seq_diagram import generate_sequence_diagram
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def after_run(
|
|
16
|
+
temp_dir: Path,
|
|
17
|
+
output_file: Optional[Union[str, Path]],
|
|
18
|
+
flow_name: str,
|
|
19
|
+
uploads_root: Optional[Path] = None,
|
|
20
|
+
skip_mmd: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Actions to perform after running the flow.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
temp_dir : Path
|
|
27
|
+
The temporary directory.
|
|
28
|
+
output_file : Optional[Union[str, Path]]
|
|
29
|
+
The output file.
|
|
30
|
+
flow_name : str
|
|
31
|
+
The flow name.
|
|
32
|
+
uploads_root : Optional[Path], optional
|
|
33
|
+
The runtime uploads root, by default None
|
|
34
|
+
skip_mmd : bool, optional
|
|
35
|
+
Whether to skip the mermaid sequence diagram generation,
|
|
36
|
+
by default, False
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(output_file, str):
|
|
39
|
+
output_file = Path(output_file)
|
|
40
|
+
mmd_dir = output_file.parent if output_file else Path.cwd()
|
|
41
|
+
if skip_mmd is False:
|
|
42
|
+
events_csv_path = temp_dir / "logs" / "events.csv"
|
|
43
|
+
if events_csv_path.exists():
|
|
44
|
+
print("Generating mermaid sequence diagram...")
|
|
45
|
+
mmd_path = temp_dir / f"{flow_name}.mmd"
|
|
46
|
+
generate_sequence_diagram(events_csv_path, mmd_path)
|
|
47
|
+
if (
|
|
48
|
+
not output_file
|
|
49
|
+
and mmd_path.exists()
|
|
50
|
+
and mmd_path != mmd_dir / f"{flow_name}.mmd"
|
|
51
|
+
):
|
|
52
|
+
try:
|
|
53
|
+
shutil.copyfile(mmd_path, mmd_dir / f"{flow_name}.mmd")
|
|
54
|
+
except BaseException: # pylint: disable=broad-exception-caught
|
|
55
|
+
pass
|
|
56
|
+
if output_file:
|
|
57
|
+
destination_dir = output_file.parent
|
|
58
|
+
destination_dir = (
|
|
59
|
+
destination_dir
|
|
60
|
+
/ "waldiez_out"
|
|
61
|
+
/ datetime.datetime.now().strftime("%Y%m%d%H%M%S")
|
|
62
|
+
)
|
|
63
|
+
destination_dir.mkdir(parents=True, exist_ok=True)
|
|
64
|
+
# copy the contents of the temp dir to the destination dir
|
|
65
|
+
print(f"Copying the results to {destination_dir}")
|
|
66
|
+
copy_results(
|
|
67
|
+
temp_dir=temp_dir,
|
|
68
|
+
output_file=output_file,
|
|
69
|
+
destination_dir=destination_dir,
|
|
70
|
+
)
|
|
71
|
+
shutil.rmtree(temp_dir)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def copy_results(
|
|
75
|
+
temp_dir: Path,
|
|
76
|
+
output_file: Path,
|
|
77
|
+
destination_dir: Path,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Copy the results to the output directory.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
temp_dir : Path
|
|
84
|
+
The temporary directory.
|
|
85
|
+
output_file : Path
|
|
86
|
+
The output file.
|
|
87
|
+
destination_dir : Path
|
|
88
|
+
The destination directory.
|
|
89
|
+
"""
|
|
90
|
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
output_dir = output_file.parent
|
|
92
|
+
for item in temp_dir.iterdir():
|
|
93
|
+
# skip cache files
|
|
94
|
+
if (
|
|
95
|
+
item.name.startswith("__pycache__")
|
|
96
|
+
or item.name.endswith(".pyc")
|
|
97
|
+
or item.name.endswith(".pyo")
|
|
98
|
+
or item.name.endswith(".pyd")
|
|
99
|
+
or item.name == ".cache"
|
|
100
|
+
):
|
|
101
|
+
continue
|
|
102
|
+
if item.is_file():
|
|
103
|
+
# let's also copy the "tree of thoughts" image
|
|
104
|
+
# to the output directory
|
|
105
|
+
if item.name.endswith("tree_of_thoughts.png") or item.name.endswith(
|
|
106
|
+
"reasoning_tree.json"
|
|
107
|
+
):
|
|
108
|
+
shutil.copy(item, output_dir / item.name)
|
|
109
|
+
shutil.copy(item, destination_dir)
|
|
110
|
+
else:
|
|
111
|
+
shutil.copytree(item, destination_dir / item.name)
|
|
112
|
+
if output_file.is_file():
|
|
113
|
+
if output_file.suffix == ".waldiez":
|
|
114
|
+
output_file = output_file.with_suffix(".py")
|
|
115
|
+
if output_file.suffix == ".py":
|
|
116
|
+
src = temp_dir / output_file.name
|
|
117
|
+
if src.exists():
|
|
118
|
+
dst = destination_dir / output_file.name
|
|
119
|
+
if dst.exists():
|
|
120
|
+
dst.unlink()
|
|
121
|
+
shutil.copyfile(src, output_dir / output_file.name)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
"""Actions to perform before running the flow."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import io
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from typing import Callable
|
|
11
|
+
|
|
12
|
+
from .environment import in_virtualenv, is_root
|
|
13
|
+
from .utils import strip_ansi
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def install_requirements(
|
|
17
|
+
extra_requirements: set[str],
|
|
18
|
+
printer: Callable[..., None] = print,
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Install the requirements.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
extra_requirements : set[str]
|
|
25
|
+
The extra requirements.
|
|
26
|
+
printer : Callable[..., None]
|
|
27
|
+
The printer function to use, defaults to print.
|
|
28
|
+
"""
|
|
29
|
+
requirements_string = ", ".join(extra_requirements)
|
|
30
|
+
printer(f"Installing requirements: {requirements_string}")
|
|
31
|
+
pip_install = [sys.executable, "-m", "pip", "install"]
|
|
32
|
+
break_system_packages = ""
|
|
33
|
+
if not in_virtualenv(): # it should
|
|
34
|
+
# if not, let's try to install as user
|
|
35
|
+
# not sure if --break-system-packages is safe,
|
|
36
|
+
# but it might fail if we don't
|
|
37
|
+
break_system_packages = os.environ.get("PIP_BREAK_SYSTEM_PACKAGES", "")
|
|
38
|
+
os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = "1"
|
|
39
|
+
if not is_root():
|
|
40
|
+
pip_install.append("--user")
|
|
41
|
+
pip_install.extend(extra_requirements)
|
|
42
|
+
# pylint: disable=too-many-try-statements
|
|
43
|
+
try:
|
|
44
|
+
with subprocess.Popen(
|
|
45
|
+
pip_install,
|
|
46
|
+
stdout=subprocess.PIPE,
|
|
47
|
+
stderr=subprocess.PIPE,
|
|
48
|
+
) as proc:
|
|
49
|
+
if proc.stdout:
|
|
50
|
+
for line in io.TextIOWrapper(proc.stdout, encoding="utf-8"):
|
|
51
|
+
printer(strip_ansi(line.strip()))
|
|
52
|
+
if proc.stderr:
|
|
53
|
+
for line in io.TextIOWrapper(proc.stderr, encoding="utf-8"):
|
|
54
|
+
printer(strip_ansi(line.strip()))
|
|
55
|
+
finally:
|
|
56
|
+
if not in_virtualenv():
|
|
57
|
+
# restore the old env var
|
|
58
|
+
if break_system_packages:
|
|
59
|
+
os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = break_system_packages
|
|
60
|
+
else:
|
|
61
|
+
del os.environ["PIP_BREAK_SYSTEM_PACKAGES"]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def a_install_requirements(
|
|
65
|
+
extra_requirements: set[str],
|
|
66
|
+
printer: Callable[..., None] = print,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Install the requirements asynchronously.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
extra_requirements : set[str]
|
|
73
|
+
The extra requirements.
|
|
74
|
+
printer : Callable[..., None]
|
|
75
|
+
The printer function to use, defaults to print.
|
|
76
|
+
"""
|
|
77
|
+
requirements_string = ", ".join(extra_requirements)
|
|
78
|
+
printer(f"Installing requirements: {requirements_string}")
|
|
79
|
+
pip_install = [sys.executable, "-m", "pip", "install"]
|
|
80
|
+
break_system_packages = ""
|
|
81
|
+
if not in_virtualenv():
|
|
82
|
+
break_system_packages = os.environ.get("PIP_BREAK_SYSTEM_PACKAGES", "")
|
|
83
|
+
os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = "1"
|
|
84
|
+
if not is_root():
|
|
85
|
+
pip_install.extend(["--user"])
|
|
86
|
+
pip_install.extend(extra_requirements)
|
|
87
|
+
# pylint: disable=too-many-try-statements
|
|
88
|
+
try:
|
|
89
|
+
proc = await asyncio.create_subprocess_exec(
|
|
90
|
+
*pip_install,
|
|
91
|
+
stdout=asyncio.subprocess.PIPE,
|
|
92
|
+
stderr=asyncio.subprocess.PIPE,
|
|
93
|
+
)
|
|
94
|
+
if proc.stdout:
|
|
95
|
+
async for line in proc.stdout:
|
|
96
|
+
printer(strip_ansi(line.decode().strip()))
|
|
97
|
+
if proc.stderr:
|
|
98
|
+
async for line in proc.stderr:
|
|
99
|
+
printer(strip_ansi(line.decode().strip()))
|
|
100
|
+
finally:
|
|
101
|
+
if not in_virtualenv():
|
|
102
|
+
if break_system_packages:
|
|
103
|
+
os.environ["PIP_BREAK_SYSTEM_PACKAGES"] = break_system_packages
|
|
104
|
+
else:
|
|
105
|
+
del os.environ["PIP_BREAK_SYSTEM_PACKAGES"]
|