waldiez 0.5.10__py3-none-any.whl → 0.6.1__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 -1
- waldiez/_version.py +1 -1
- waldiez/cli.py +19 -7
- waldiez/cli_extras/jupyter.py +3 -0
- waldiez/cli_extras/runner.py +3 -1
- waldiez/cli_extras/studio.py +3 -1
- waldiez/exporter.py +9 -3
- waldiez/exporting/agent/exporter.py +15 -16
- waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
- waldiez/exporting/agent/extras/group_member_extras.py +6 -5
- waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
- waldiez/exporting/agent/extras/handoffs/available.py +2 -1
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
- waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
- waldiez/exporting/agent/extras/handoffs/target.py +7 -4
- waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
- waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
- waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
- waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
- waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
- waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
- waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/exporter.py +4 -4
- waldiez/exporting/chats/processor.py +1 -2
- waldiez/exporting/chats/utils/common.py +89 -48
- waldiez/exporting/chats/utils/group.py +9 -9
- waldiez/exporting/chats/utils/nested.py +7 -7
- waldiez/exporting/chats/utils/sequential.py +1 -1
- waldiez/exporting/chats/utils/single.py +2 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/content.py +7 -7
- waldiez/exporting/core/context.py +5 -3
- waldiez/exporting/core/exporter.py +5 -3
- waldiez/exporting/core/exporters.py +2 -2
- waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
- waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
- waldiez/exporting/core/extras/base.py +7 -5
- waldiez/exporting/core/extras/flow_extras.py +4 -5
- waldiez/exporting/core/extras/model_extras.py +2 -2
- waldiez/exporting/core/extras/path_resolver.py +1 -2
- waldiez/exporting/core/extras/serializer.py +13 -11
- waldiez/exporting/core/protocols.py +6 -5
- waldiez/exporting/core/result.py +25 -28
- waldiez/exporting/core/types.py +11 -10
- waldiez/exporting/core/utils/llm_config.py +4 -4
- waldiez/exporting/core/validation.py +10 -11
- waldiez/exporting/flow/execution_generator.py +99 -10
- waldiez/exporting/flow/exporter.py +2 -2
- waldiez/exporting/flow/factory.py +2 -2
- waldiez/exporting/flow/file_generator.py +4 -2
- waldiez/exporting/flow/merger.py +5 -3
- waldiez/exporting/flow/orchestrator.py +72 -2
- waldiez/exporting/flow/utils/common.py +6 -6
- waldiez/exporting/flow/utils/importing.py +7 -8
- waldiez/exporting/flow/utils/linting.py +25 -9
- waldiez/exporting/flow/utils/logging.py +5 -77
- waldiez/exporting/models/exporter.py +8 -8
- waldiez/exporting/models/processor.py +5 -5
- waldiez/exporting/tools/exporter.py +2 -2
- waldiez/exporting/tools/processor.py +7 -4
- waldiez/io/__init__.py +11 -5
- waldiez/io/_ws.py +12 -6
- waldiez/io/models/constants.py +10 -10
- waldiez/io/models/content/audio.py +1 -0
- waldiez/io/models/content/base.py +20 -18
- waldiez/io/models/content/file.py +1 -0
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/content/text.py +1 -0
- waldiez/io/models/content/video.py +1 -0
- waldiez/io/models/user_input.py +10 -5
- waldiez/io/models/user_response.py +17 -16
- waldiez/io/mqtt.py +18 -31
- waldiez/io/redis.py +18 -22
- waldiez/io/structured.py +122 -70
- waldiez/io/utils.py +19 -10
- waldiez/io/ws.py +7 -3
- waldiez/logger.py +16 -3
- waldiez/models/agents/__init__.py +3 -0
- waldiez/models/agents/agent/agent.py +25 -17
- waldiez/models/agents/agent/agent_data.py +25 -22
- waldiez/models/agents/agent/code_execution.py +9 -11
- waldiez/models/agents/agent/termination_message.py +10 -12
- waldiez/models/agents/agent/update_system_message.py +2 -4
- waldiez/models/agents/agents.py +8 -8
- waldiez/models/agents/assistant/assistant.py +6 -3
- waldiez/models/agents/assistant/assistant_data.py +2 -2
- waldiez/models/agents/captain/captain_agent.py +7 -4
- waldiez/models/agents/captain/captain_agent_data.py +5 -7
- waldiez/models/agents/doc_agent/doc_agent.py +7 -4
- waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
- waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
- waldiez/models/agents/extra_requirements.py +3 -3
- waldiez/models/agents/group_manager/group_manager.py +12 -7
- waldiez/models/agents/group_manager/group_manager_data.py +13 -12
- waldiez/models/agents/group_manager/speakers.py +17 -19
- waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
- waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
- waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
- waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
- waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
- waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
- waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
- waldiez/models/agents/user_proxy/user_proxy.py +6 -3
- waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
- waldiez/models/chat/chat.py +28 -20
- waldiez/models/chat/chat_data.py +22 -21
- waldiez/models/chat/chat_message.py +9 -9
- waldiez/models/chat/chat_nested.py +9 -9
- waldiez/models/chat/chat_summary.py +6 -6
- waldiez/models/common/__init__.py +2 -0
- waldiez/models/common/ag2_version.py +2 -0
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +8 -6
- waldiez/models/common/handoff.py +20 -17
- waldiez/models/common/method_utils.py +9 -7
- waldiez/models/common/naming.py +49 -0
- waldiez/models/flow/flow.py +11 -6
- waldiez/models/flow/flow_data.py +23 -17
- waldiez/models/flow/info.py +3 -3
- waldiez/models/flow/naming.py +2 -1
- waldiez/models/model/_aws.py +11 -13
- waldiez/models/model/_llm.py +8 -0
- waldiez/models/model/_price.py +2 -4
- waldiez/models/model/extra_requirements.py +1 -3
- waldiez/models/model/model.py +2 -2
- waldiez/models/model/model_data.py +21 -21
- waldiez/models/tool/extra_requirements.py +2 -4
- waldiez/models/tool/predefined/_duckduckgo.py +1 -0
- waldiez/models/tool/predefined/_email.py +4 -0
- waldiez/models/tool/predefined/_google.py +1 -0
- waldiez/models/tool/predefined/_perplexity.py +2 -1
- waldiez/models/tool/predefined/_searxng.py +2 -1
- waldiez/models/tool/predefined/_tavily.py +1 -0
- waldiez/models/tool/predefined/_wikipedia.py +2 -1
- waldiez/models/tool/predefined/_youtube.py +1 -0
- waldiez/models/tool/tool.py +8 -5
- waldiez/models/tool/tool_data.py +2 -2
- waldiez/models/waldiez.py +152 -4
- waldiez/runner.py +11 -5
- waldiez/running/async_utils.py +192 -0
- waldiez/running/base_runner.py +155 -241
- waldiez/running/dir_utils.py +52 -0
- waldiez/running/environment.py +10 -44
- waldiez/running/events_mixin.py +252 -0
- waldiez/running/exceptions.py +20 -0
- waldiez/running/gen_seq_diagram.py +18 -15
- waldiez/running/io_utils.py +216 -0
- waldiez/running/protocol.py +11 -5
- waldiez/running/requirements_mixin.py +65 -0
- waldiez/running/results_mixin.py +926 -0
- waldiez/running/standard_runner.py +24 -27
- waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
- waldiez/running/step_by_step/command_handler.py +154 -0
- waldiez/running/step_by_step/events_processor.py +379 -0
- waldiez/running/step_by_step/step_by_step_models.py +425 -41
- waldiez/running/step_by_step/step_by_step_runner.py +437 -382
- waldiez/running/subprocess_runner/__base__.py +13 -8
- waldiez/running/subprocess_runner/_async_runner.py +6 -4
- waldiez/running/subprocess_runner/_sync_runner.py +11 -6
- waldiez/running/subprocess_runner/runner.py +48 -23
- waldiez/running/timeline_processor.py +1 -1
- waldiez/utils/__init__.py +2 -0
- waldiez/utils/conflict_checker.py +4 -4
- waldiez/utils/python_manager.py +415 -0
- waldiez/ws/__init__.py +8 -7
- waldiez/ws/_file_handler.py +18 -20
- waldiez/ws/_mock.py +75 -0
- waldiez/ws/cli.py +58 -10
- waldiez/ws/client_manager.py +77 -53
- waldiez/ws/errors.py +3 -0
- waldiez/ws/models.py +61 -53
- waldiez/ws/reloader.py +33 -4
- waldiez/ws/server.py +121 -52
- waldiez/ws/session_manager.py +8 -9
- waldiez/ws/session_stats.py +1 -1
- waldiez/ws/utils.py +33 -5
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
- waldiez-0.6.1.dist-info/RECORD +254 -0
- waldiez/running/post_run.py +0 -180
- waldiez/running/pre_run.py +0 -159
- waldiez/running/run_results.py +0 -14
- waldiez/running/utils.py +0 -511
- waldiez-0.5.10.dist-info/RECORD +0 -248
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/NOTICE.md +0 -0
waldiez/running/utils.py
DELETED
|
@@ -1,511 +0,0 @@
|
|
|
1
|
-
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
-
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
-
"""Common utilities for the waldiez runner."""
|
|
4
|
-
|
|
5
|
-
import asyncio
|
|
6
|
-
import functools
|
|
7
|
-
import inspect
|
|
8
|
-
import logging
|
|
9
|
-
import os
|
|
10
|
-
import re
|
|
11
|
-
import subprocess
|
|
12
|
-
import sys
|
|
13
|
-
import threading
|
|
14
|
-
import traceback
|
|
15
|
-
|
|
16
|
-
# noinspection PyProtectedMember
|
|
17
|
-
from asyncio.subprocess import Process
|
|
18
|
-
from contextlib import asynccontextmanager, contextmanager
|
|
19
|
-
from dataclasses import dataclass
|
|
20
|
-
from getpass import getpass
|
|
21
|
-
from pathlib import Path
|
|
22
|
-
from typing import (
|
|
23
|
-
Any,
|
|
24
|
-
AsyncIterator,
|
|
25
|
-
Callable,
|
|
26
|
-
Coroutine,
|
|
27
|
-
Generic,
|
|
28
|
-
Iterator,
|
|
29
|
-
TypeVar,
|
|
30
|
-
Union,
|
|
31
|
-
cast,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
T = TypeVar("T")
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
@dataclass
|
|
39
|
-
class _ResultContainer(Generic[T]):
|
|
40
|
-
"""Container for thread execution results with proper typing."""
|
|
41
|
-
|
|
42
|
-
result: T | None = None
|
|
43
|
-
exception: BaseException | None = None
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
@dataclass
|
|
47
|
-
class ProcessSetup:
|
|
48
|
-
"""Container for subprocess setup data."""
|
|
49
|
-
|
|
50
|
-
temp_dir: Path
|
|
51
|
-
file_path: Path
|
|
52
|
-
old_vars: dict[str, str]
|
|
53
|
-
skip_mmd: bool
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
@contextmanager
|
|
57
|
-
def chdir(to: Union[str, Path]) -> Iterator[None]:
|
|
58
|
-
"""Change the current working directory in a context.
|
|
59
|
-
|
|
60
|
-
Parameters
|
|
61
|
-
----------
|
|
62
|
-
to : Union[str, Path]
|
|
63
|
-
The directory to change to.
|
|
64
|
-
|
|
65
|
-
Yields
|
|
66
|
-
------
|
|
67
|
-
Iterator[None]
|
|
68
|
-
The context manager.
|
|
69
|
-
"""
|
|
70
|
-
old_cwd = str(os.getcwd())
|
|
71
|
-
os.chdir(to)
|
|
72
|
-
try:
|
|
73
|
-
yield
|
|
74
|
-
finally:
|
|
75
|
-
os.chdir(old_cwd)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@asynccontextmanager
|
|
79
|
-
async def a_chdir(to: Union[str, Path]) -> AsyncIterator[None]:
|
|
80
|
-
"""Asynchronously change the current working directory in a context.
|
|
81
|
-
|
|
82
|
-
Parameters
|
|
83
|
-
----------
|
|
84
|
-
to : Union[str, Path]
|
|
85
|
-
The directory to change to.
|
|
86
|
-
|
|
87
|
-
Yields
|
|
88
|
-
------
|
|
89
|
-
AsyncIterator[None]
|
|
90
|
-
The async context manager.
|
|
91
|
-
"""
|
|
92
|
-
old_cwd = str(os.getcwd())
|
|
93
|
-
os.chdir(to)
|
|
94
|
-
try:
|
|
95
|
-
yield
|
|
96
|
-
finally:
|
|
97
|
-
os.chdir(old_cwd)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def strip_ansi(text: str) -> str:
|
|
101
|
-
"""Remove ANSI escape sequences from text.
|
|
102
|
-
|
|
103
|
-
Parameters
|
|
104
|
-
----------
|
|
105
|
-
text : str
|
|
106
|
-
The text to strip.
|
|
107
|
-
|
|
108
|
-
Returns
|
|
109
|
-
-------
|
|
110
|
-
str
|
|
111
|
-
The text without ANSI escape sequences.
|
|
112
|
-
"""
|
|
113
|
-
ansi_pattern = re.compile(r"\x1b\[[0-9;]*m|\x1b\[.*?[@-~]")
|
|
114
|
-
return ansi_pattern.sub("", text)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
def create_sync_subprocess(setup: ProcessSetup) -> subprocess.Popen[bytes]:
|
|
118
|
-
"""Create a synchronous subprocess.
|
|
119
|
-
|
|
120
|
-
Parameters
|
|
121
|
-
----------
|
|
122
|
-
setup : ProcessSetup
|
|
123
|
-
The setup data for the subprocess.
|
|
124
|
-
|
|
125
|
-
Returns
|
|
126
|
-
-------
|
|
127
|
-
subprocess.Popen[bytes]
|
|
128
|
-
The created subprocess.
|
|
129
|
-
"""
|
|
130
|
-
return subprocess.Popen(
|
|
131
|
-
[sys.executable, "-u", str(setup.file_path)],
|
|
132
|
-
stdout=subprocess.PIPE,
|
|
133
|
-
stderr=subprocess.PIPE,
|
|
134
|
-
stdin=subprocess.PIPE,
|
|
135
|
-
# text=True,
|
|
136
|
-
# bufsize=1, # Line buffered for real-time output
|
|
137
|
-
# universal_newlines=True,
|
|
138
|
-
env={**os.environ},
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
async def create_async_subprocess(setup: ProcessSetup) -> Process:
|
|
143
|
-
"""Create an asynchronous subprocess.
|
|
144
|
-
|
|
145
|
-
Parameters
|
|
146
|
-
----------
|
|
147
|
-
setup : ProcessSetup
|
|
148
|
-
The setup data for the subprocess.
|
|
149
|
-
|
|
150
|
-
Returns
|
|
151
|
-
-------
|
|
152
|
-
Process
|
|
153
|
-
The created asynchronous subprocess.
|
|
154
|
-
"""
|
|
155
|
-
return await asyncio.create_subprocess_exec(
|
|
156
|
-
sys.executable,
|
|
157
|
-
"-u",
|
|
158
|
-
str(setup.file_path),
|
|
159
|
-
# stdout=asyncio.subprocess.PIPE,
|
|
160
|
-
# stderr=asyncio.subprocess.PIPE,
|
|
161
|
-
# stdin=asyncio.subprocess.PIPE,
|
|
162
|
-
env={**os.environ},
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
async def input_async(prompt: str, *, password: bool = False) -> str:
|
|
167
|
-
"""Asynchronous input function.
|
|
168
|
-
|
|
169
|
-
Parameters
|
|
170
|
-
----------
|
|
171
|
-
prompt : str
|
|
172
|
-
The prompt to display to the user.
|
|
173
|
-
password : bool, optional
|
|
174
|
-
Whether to hide input (password mode), by default False.
|
|
175
|
-
|
|
176
|
-
Returns
|
|
177
|
-
-------
|
|
178
|
-
str
|
|
179
|
-
The user input.
|
|
180
|
-
"""
|
|
181
|
-
if password:
|
|
182
|
-
try:
|
|
183
|
-
return await asyncio.to_thread(getpass, prompt)
|
|
184
|
-
except EOFError:
|
|
185
|
-
return ""
|
|
186
|
-
try:
|
|
187
|
-
return await asyncio.to_thread(input, prompt)
|
|
188
|
-
except EOFError:
|
|
189
|
-
return ""
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def input_sync(prompt: str, *, password: bool = False) -> str:
|
|
193
|
-
"""Input function (synchronous).
|
|
194
|
-
|
|
195
|
-
Parameters
|
|
196
|
-
----------
|
|
197
|
-
prompt : str
|
|
198
|
-
The prompt to display to the user.
|
|
199
|
-
password : bool, optional
|
|
200
|
-
Whether to hide input (password mode), by default False.
|
|
201
|
-
|
|
202
|
-
Returns
|
|
203
|
-
-------
|
|
204
|
-
str
|
|
205
|
-
The user input.
|
|
206
|
-
"""
|
|
207
|
-
if password:
|
|
208
|
-
try:
|
|
209
|
-
return getpass(prompt)
|
|
210
|
-
except EOFError:
|
|
211
|
-
return ""
|
|
212
|
-
try:
|
|
213
|
-
return input(prompt)
|
|
214
|
-
except EOFError:
|
|
215
|
-
return ""
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
# pylint: disable=import-outside-toplevel,too-complex
|
|
219
|
-
def get_printer() -> Callable[..., None]: # noqa: C901
|
|
220
|
-
"""Get the printer function.
|
|
221
|
-
|
|
222
|
-
Returns
|
|
223
|
-
-------
|
|
224
|
-
Callable[..., None]
|
|
225
|
-
The printer function that handles Unicode encoding errors gracefully.
|
|
226
|
-
"""
|
|
227
|
-
try:
|
|
228
|
-
# noinspection PyUnresolvedReferences
|
|
229
|
-
from autogen.io import IOStream # type: ignore
|
|
230
|
-
|
|
231
|
-
printer = IOStream.get_default().print
|
|
232
|
-
except ImportError: # pragma: no cover
|
|
233
|
-
# Fallback to standard print if autogen is not available
|
|
234
|
-
printer = print
|
|
235
|
-
|
|
236
|
-
# noinspection PyBroadException,TryExceptPass
|
|
237
|
-
def safe_printer(*args: Any, **kwargs: Any) -> None: # noqa: C901
|
|
238
|
-
"""Safe printer that handles Unicode encoding errors.
|
|
239
|
-
|
|
240
|
-
Parameters
|
|
241
|
-
----------
|
|
242
|
-
*args : Any
|
|
243
|
-
Arguments to pass to the printer
|
|
244
|
-
**kwargs : Any
|
|
245
|
-
Keyword arguments to pass to the printer
|
|
246
|
-
"""
|
|
247
|
-
# pylint: disable=broad-exception-caught,too-many-try-statements
|
|
248
|
-
try:
|
|
249
|
-
printer(*args, **kwargs)
|
|
250
|
-
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
251
|
-
# First fallback: try to get a safe string representation
|
|
252
|
-
try:
|
|
253
|
-
msg, flush = get_what_to_print(*args, **kwargs)
|
|
254
|
-
# Convert problematic characters to safe representations
|
|
255
|
-
safe_msg = msg.encode("utf-8", errors="replace").decode("utf-8")
|
|
256
|
-
printer(safe_msg, end="", flush=flush)
|
|
257
|
-
except (UnicodeEncodeError, UnicodeDecodeError):
|
|
258
|
-
# Second fallback: use built-in print with safe encoding
|
|
259
|
-
try:
|
|
260
|
-
# Convert args to safe string representations
|
|
261
|
-
safe_args: list[str] = []
|
|
262
|
-
for arg in args:
|
|
263
|
-
try:
|
|
264
|
-
safe_args.append(
|
|
265
|
-
str(arg)
|
|
266
|
-
.encode("utf-8", errors="replace")
|
|
267
|
-
.decode("utf-8")
|
|
268
|
-
)
|
|
269
|
-
except (
|
|
270
|
-
UnicodeEncodeError,
|
|
271
|
-
UnicodeDecodeError,
|
|
272
|
-
): # pragma: no cover
|
|
273
|
-
safe_args.append(repr(arg))
|
|
274
|
-
|
|
275
|
-
# Use built-in print instead of the custom printer
|
|
276
|
-
print(*safe_args, **kwargs)
|
|
277
|
-
|
|
278
|
-
except Exception:
|
|
279
|
-
# Final fallback: write directly to stderr buffer
|
|
280
|
-
error_msg = (
|
|
281
|
-
"Could not print the message due to encoding issues.\n"
|
|
282
|
-
)
|
|
283
|
-
to_sys_stderr(error_msg)
|
|
284
|
-
except Exception as e:
|
|
285
|
-
# Handle any other unexpected errors
|
|
286
|
-
traceback.print_exc()
|
|
287
|
-
error_msg = f"Unexpected error in printer: {str(e)}\n"
|
|
288
|
-
to_sys_stderr(error_msg)
|
|
289
|
-
|
|
290
|
-
return safe_printer
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
def to_sys_stderr(msg: str) -> None:
|
|
294
|
-
"""Write a message to sys.stderr.
|
|
295
|
-
|
|
296
|
-
Parameters
|
|
297
|
-
----------
|
|
298
|
-
msg : str
|
|
299
|
-
The message to write to stderr.
|
|
300
|
-
"""
|
|
301
|
-
# pylint: disable=broad-exception-caught
|
|
302
|
-
# noinspection TryExceptPass,PyBroadException
|
|
303
|
-
try:
|
|
304
|
-
if hasattr(sys.stderr, "buffer"):
|
|
305
|
-
sys.stderr.buffer.write(msg.encode("utf-8", errors="replace"))
|
|
306
|
-
sys.stderr.buffer.flush()
|
|
307
|
-
else: # pragma: no cover
|
|
308
|
-
sys.stderr.write(msg)
|
|
309
|
-
sys.stderr.flush()
|
|
310
|
-
except Exception: # pragma: no cover
|
|
311
|
-
pass
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def get_what_to_print(*args: Any, **kwargs: Any) -> tuple[str, bool]:
|
|
315
|
-
"""Extract message and flush flag from print arguments.
|
|
316
|
-
|
|
317
|
-
Parameters
|
|
318
|
-
----------
|
|
319
|
-
*args : Any
|
|
320
|
-
Arguments to print
|
|
321
|
-
**kwargs : Any
|
|
322
|
-
Keyword arguments for print function
|
|
323
|
-
|
|
324
|
-
Returns
|
|
325
|
-
-------
|
|
326
|
-
tuple[str, bool]
|
|
327
|
-
Message to print and flush flag
|
|
328
|
-
"""
|
|
329
|
-
# Convert all args to strings and join with spaces (like print does)
|
|
330
|
-
msg = " ".join(str(arg) for arg in args)
|
|
331
|
-
|
|
332
|
-
# Handle sep parameter
|
|
333
|
-
sep = kwargs.get("sep", " ")
|
|
334
|
-
if isinstance(sep, bytes): # pragma: no cover
|
|
335
|
-
sep = sep.decode("utf-8", errors="replace")
|
|
336
|
-
if len(args) > 1:
|
|
337
|
-
msg = sep.join(str(arg) for arg in args)
|
|
338
|
-
|
|
339
|
-
# Handle end parameter
|
|
340
|
-
end = kwargs.get("end", "\n")
|
|
341
|
-
if isinstance(end, bytes): # pragma: no cover
|
|
342
|
-
end = end.decode("utf-8", errors="replace")
|
|
343
|
-
msg += end
|
|
344
|
-
|
|
345
|
-
# Handle flush parameter
|
|
346
|
-
flush = kwargs.get("flush", False)
|
|
347
|
-
# noinspection PyUnreachableCode
|
|
348
|
-
if not isinstance(flush, bool): # pragma: no cover
|
|
349
|
-
flush = False
|
|
350
|
-
|
|
351
|
-
return msg, flush
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
def is_async_callable(fn: Any) -> bool:
|
|
355
|
-
"""Check if a function is async callable, including partials/callables.
|
|
356
|
-
|
|
357
|
-
Parameters
|
|
358
|
-
----------
|
|
359
|
-
fn : Any
|
|
360
|
-
The function to check.
|
|
361
|
-
|
|
362
|
-
Returns
|
|
363
|
-
-------
|
|
364
|
-
bool
|
|
365
|
-
True if the function is async callable, False otherwise.
|
|
366
|
-
"""
|
|
367
|
-
if isinstance(fn, functools.partial):
|
|
368
|
-
fn = fn.func
|
|
369
|
-
unwrapped = inspect.unwrap(fn)
|
|
370
|
-
return inspect.iscoroutinefunction(
|
|
371
|
-
unwrapped
|
|
372
|
-
) or inspect.iscoroutinefunction(
|
|
373
|
-
getattr(unwrapped, "__call__", None), # noqa: B004
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
def syncify(
|
|
378
|
-
async_func: Callable[..., Coroutine[Any, Any, T]],
|
|
379
|
-
timeout: float | None = None,
|
|
380
|
-
) -> Callable[..., T]:
|
|
381
|
-
"""Convert an async function to a sync function.
|
|
382
|
-
|
|
383
|
-
This function handles the conversion of async functions to sync functions,
|
|
384
|
-
properly managing event loops and thread execution contexts.
|
|
385
|
-
|
|
386
|
-
Parameters
|
|
387
|
-
----------
|
|
388
|
-
async_func : Callable[..., Coroutine[Any, Any, T]]
|
|
389
|
-
The async function to convert.
|
|
390
|
-
timeout : float | None, optional
|
|
391
|
-
The timeout for the sync function. Defaults to None.
|
|
392
|
-
|
|
393
|
-
Returns
|
|
394
|
-
-------
|
|
395
|
-
Callable[..., T]
|
|
396
|
-
The converted sync function.
|
|
397
|
-
|
|
398
|
-
Raises
|
|
399
|
-
------
|
|
400
|
-
TimeoutError
|
|
401
|
-
If the async function times out.
|
|
402
|
-
RuntimeError
|
|
403
|
-
If there are issues with event loop management.
|
|
404
|
-
"""
|
|
405
|
-
|
|
406
|
-
def _sync_wrapper(*args: Any, **kwargs: Any) -> T:
|
|
407
|
-
"""Get the result of the async function."""
|
|
408
|
-
# pylint: disable=too-many-try-statements
|
|
409
|
-
try:
|
|
410
|
-
# Check if we're already in an event loop
|
|
411
|
-
asyncio.get_running_loop()
|
|
412
|
-
return _run_in_thread(async_func, args, kwargs, timeout)
|
|
413
|
-
except RuntimeError:
|
|
414
|
-
# No event loop running, we can use asyncio.run directly
|
|
415
|
-
logger.debug("No event loop running, using asyncio.run")
|
|
416
|
-
|
|
417
|
-
# Create a new event loop and run the coroutine
|
|
418
|
-
try:
|
|
419
|
-
if timeout is not None:
|
|
420
|
-
# Need to run with asyncio.run and wait_for inside
|
|
421
|
-
async def _with_timeout() -> T:
|
|
422
|
-
return await asyncio.wait_for(
|
|
423
|
-
async_func(*args, **kwargs), timeout=timeout
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
return asyncio.run(_with_timeout())
|
|
427
|
-
return asyncio.run(async_func(*args, **kwargs))
|
|
428
|
-
except (asyncio.TimeoutError, TimeoutError) as e:
|
|
429
|
-
raise TimeoutError(
|
|
430
|
-
f"Async function timed out after {timeout} seconds"
|
|
431
|
-
) from e
|
|
432
|
-
|
|
433
|
-
return _sync_wrapper
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
def _run_in_thread(
|
|
437
|
-
async_func: Callable[..., Coroutine[Any, Any, T]],
|
|
438
|
-
args: tuple[Any, ...],
|
|
439
|
-
kwargs: dict[str, Any],
|
|
440
|
-
timeout: float | None,
|
|
441
|
-
) -> T:
|
|
442
|
-
"""Run async function in a separate thread.
|
|
443
|
-
|
|
444
|
-
Parameters
|
|
445
|
-
----------
|
|
446
|
-
async_func : Callable[..., Coroutine[Any, Any, T]]
|
|
447
|
-
The async function to run.
|
|
448
|
-
args : tuple[Any, ...]
|
|
449
|
-
Positional arguments for the function.
|
|
450
|
-
kwargs : dict[str, Any]
|
|
451
|
-
Keyword arguments for the function.
|
|
452
|
-
timeout : float | None
|
|
453
|
-
Timeout in seconds.
|
|
454
|
-
|
|
455
|
-
Returns
|
|
456
|
-
-------
|
|
457
|
-
T
|
|
458
|
-
The result of the async function.
|
|
459
|
-
|
|
460
|
-
Raises
|
|
461
|
-
------
|
|
462
|
-
TimeoutError
|
|
463
|
-
If the function execution times out.
|
|
464
|
-
RuntimeError
|
|
465
|
-
If thread execution fails unexpectedly.
|
|
466
|
-
"""
|
|
467
|
-
result_container: _ResultContainer[T] = _ResultContainer()
|
|
468
|
-
finished_event = threading.Event()
|
|
469
|
-
|
|
470
|
-
def _thread_target() -> None:
|
|
471
|
-
"""Target function for the thread."""
|
|
472
|
-
# pylint: disable=too-many-try-statements, broad-exception-caught
|
|
473
|
-
try:
|
|
474
|
-
if timeout is not None:
|
|
475
|
-
|
|
476
|
-
async def _with_timeout() -> T:
|
|
477
|
-
return await asyncio.wait_for(
|
|
478
|
-
async_func(*args, **kwargs), timeout=timeout
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
result_container.result = asyncio.run(_with_timeout())
|
|
482
|
-
else:
|
|
483
|
-
result_container.result = asyncio.run(
|
|
484
|
-
async_func(*args, **kwargs)
|
|
485
|
-
)
|
|
486
|
-
except (
|
|
487
|
-
BaseException
|
|
488
|
-
) as e: # Catch BaseException to propagate cancellations
|
|
489
|
-
result_container.exception = e
|
|
490
|
-
finally:
|
|
491
|
-
finished_event.set()
|
|
492
|
-
|
|
493
|
-
thread = threading.Thread(target=_thread_target, daemon=True)
|
|
494
|
-
thread.start()
|
|
495
|
-
|
|
496
|
-
# Wait for completion with timeout
|
|
497
|
-
timeout_buffer = 1.0 # 1 second buffer for cleanup
|
|
498
|
-
wait_timeout = timeout + timeout_buffer if timeout is not None else None
|
|
499
|
-
|
|
500
|
-
if not finished_event.wait(timeout=wait_timeout): # pragma: no cover
|
|
501
|
-
raise TimeoutError(
|
|
502
|
-
f"Function execution timed out after {timeout} seconds"
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
thread.join(timeout=timeout_buffer) # Give thread time to clean up
|
|
506
|
-
|
|
507
|
-
if result_container.exception is not None:
|
|
508
|
-
raise result_container.exception
|
|
509
|
-
|
|
510
|
-
# Use cast since we know the result should be T if no exception occurred
|
|
511
|
-
return cast(T, result_container.result)
|