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.

Files changed (192) hide show
  1. waldiez/__init__.py +1 -1
  2. waldiez/_version.py +1 -1
  3. waldiez/cli.py +19 -7
  4. waldiez/cli_extras/jupyter.py +3 -0
  5. waldiez/cli_extras/runner.py +3 -1
  6. waldiez/cli_extras/studio.py +3 -1
  7. waldiez/exporter.py +9 -3
  8. waldiez/exporting/agent/exporter.py +15 -16
  9. waldiez/exporting/agent/extras/captain_agent_extras.py +6 -6
  10. waldiez/exporting/agent/extras/doc_agent_extras.py +6 -6
  11. waldiez/exporting/agent/extras/group_manager_agent_extas.py +40 -24
  12. waldiez/exporting/agent/extras/group_member_extras.py +6 -5
  13. waldiez/exporting/agent/extras/handoffs/after_work.py +2 -1
  14. waldiez/exporting/agent/extras/handoffs/available.py +2 -1
  15. waldiez/exporting/agent/extras/handoffs/condition.py +3 -2
  16. waldiez/exporting/agent/extras/handoffs/handoff.py +2 -1
  17. waldiez/exporting/agent/extras/handoffs/target.py +7 -4
  18. waldiez/exporting/agent/extras/rag/chroma_extras.py +27 -19
  19. waldiez/exporting/agent/extras/rag/mongo_extras.py +8 -8
  20. waldiez/exporting/agent/extras/rag/pgvector_extras.py +5 -5
  21. waldiez/exporting/agent/extras/rag/qdrant_extras.py +5 -4
  22. waldiez/exporting/agent/extras/rag/vector_db_extras.py +1 -1
  23. waldiez/exporting/agent/extras/rag_user_proxy_agent_extras.py +5 -7
  24. waldiez/exporting/agent/extras/reasoning_agent_extras.py +3 -5
  25. waldiez/exporting/agent/termination.py +1 -0
  26. waldiez/exporting/chats/exporter.py +4 -4
  27. waldiez/exporting/chats/processor.py +1 -2
  28. waldiez/exporting/chats/utils/common.py +89 -48
  29. waldiez/exporting/chats/utils/group.py +9 -9
  30. waldiez/exporting/chats/utils/nested.py +7 -7
  31. waldiez/exporting/chats/utils/sequential.py +1 -1
  32. waldiez/exporting/chats/utils/single.py +2 -2
  33. waldiez/exporting/core/constants.py +3 -1
  34. waldiez/exporting/core/content.py +7 -7
  35. waldiez/exporting/core/context.py +5 -3
  36. waldiez/exporting/core/exporter.py +5 -3
  37. waldiez/exporting/core/exporters.py +2 -2
  38. waldiez/exporting/core/extras/agent_extras/captain_extras.py +2 -2
  39. waldiez/exporting/core/extras/agent_extras/group_manager_extras.py +2 -2
  40. waldiez/exporting/core/extras/agent_extras/rag_user_extras.py +2 -2
  41. waldiez/exporting/core/extras/agent_extras/standard_extras.py +3 -8
  42. waldiez/exporting/core/extras/base.py +7 -5
  43. waldiez/exporting/core/extras/flow_extras.py +4 -5
  44. waldiez/exporting/core/extras/model_extras.py +2 -2
  45. waldiez/exporting/core/extras/path_resolver.py +1 -2
  46. waldiez/exporting/core/extras/serializer.py +13 -11
  47. waldiez/exporting/core/protocols.py +6 -5
  48. waldiez/exporting/core/result.py +25 -28
  49. waldiez/exporting/core/types.py +11 -10
  50. waldiez/exporting/core/utils/llm_config.py +4 -4
  51. waldiez/exporting/core/validation.py +10 -11
  52. waldiez/exporting/flow/execution_generator.py +99 -10
  53. waldiez/exporting/flow/exporter.py +2 -2
  54. waldiez/exporting/flow/factory.py +2 -2
  55. waldiez/exporting/flow/file_generator.py +4 -2
  56. waldiez/exporting/flow/merger.py +5 -3
  57. waldiez/exporting/flow/orchestrator.py +72 -2
  58. waldiez/exporting/flow/utils/common.py +6 -6
  59. waldiez/exporting/flow/utils/importing.py +7 -8
  60. waldiez/exporting/flow/utils/linting.py +25 -9
  61. waldiez/exporting/flow/utils/logging.py +5 -77
  62. waldiez/exporting/models/exporter.py +8 -8
  63. waldiez/exporting/models/processor.py +5 -5
  64. waldiez/exporting/tools/exporter.py +2 -2
  65. waldiez/exporting/tools/processor.py +7 -4
  66. waldiez/io/__init__.py +11 -5
  67. waldiez/io/_ws.py +12 -6
  68. waldiez/io/models/constants.py +10 -10
  69. waldiez/io/models/content/audio.py +1 -0
  70. waldiez/io/models/content/base.py +20 -18
  71. waldiez/io/models/content/file.py +1 -0
  72. waldiez/io/models/content/image.py +1 -0
  73. waldiez/io/models/content/text.py +1 -0
  74. waldiez/io/models/content/video.py +1 -0
  75. waldiez/io/models/user_input.py +10 -5
  76. waldiez/io/models/user_response.py +17 -16
  77. waldiez/io/mqtt.py +18 -31
  78. waldiez/io/redis.py +18 -22
  79. waldiez/io/structured.py +122 -70
  80. waldiez/io/utils.py +19 -10
  81. waldiez/io/ws.py +7 -3
  82. waldiez/logger.py +16 -3
  83. waldiez/models/agents/__init__.py +3 -0
  84. waldiez/models/agents/agent/agent.py +25 -17
  85. waldiez/models/agents/agent/agent_data.py +25 -22
  86. waldiez/models/agents/agent/code_execution.py +9 -11
  87. waldiez/models/agents/agent/termination_message.py +10 -12
  88. waldiez/models/agents/agent/update_system_message.py +2 -4
  89. waldiez/models/agents/agents.py +8 -8
  90. waldiez/models/agents/assistant/assistant.py +6 -3
  91. waldiez/models/agents/assistant/assistant_data.py +2 -2
  92. waldiez/models/agents/captain/captain_agent.py +7 -4
  93. waldiez/models/agents/captain/captain_agent_data.py +5 -7
  94. waldiez/models/agents/doc_agent/doc_agent.py +7 -4
  95. waldiez/models/agents/doc_agent/doc_agent_data.py +9 -10
  96. waldiez/models/agents/doc_agent/rag_query_engine.py +10 -12
  97. waldiez/models/agents/extra_requirements.py +3 -3
  98. waldiez/models/agents/group_manager/group_manager.py +12 -7
  99. waldiez/models/agents/group_manager/group_manager_data.py +13 -12
  100. waldiez/models/agents/group_manager/speakers.py +17 -19
  101. waldiez/models/agents/rag_user_proxy/rag_user_proxy.py +7 -4
  102. waldiez/models/agents/rag_user_proxy/rag_user_proxy_data.py +4 -1
  103. waldiez/models/agents/rag_user_proxy/retrieve_config.py +69 -63
  104. waldiez/models/agents/rag_user_proxy/vector_db_config.py +19 -19
  105. waldiez/models/agents/reasoning/reasoning_agent.py +7 -4
  106. waldiez/models/agents/reasoning/reasoning_agent_data.py +3 -2
  107. waldiez/models/agents/reasoning/reasoning_agent_reason_config.py +8 -8
  108. waldiez/models/agents/user_proxy/user_proxy.py +6 -3
  109. waldiez/models/agents/user_proxy/user_proxy_data.py +1 -1
  110. waldiez/models/chat/chat.py +28 -20
  111. waldiez/models/chat/chat_data.py +22 -21
  112. waldiez/models/chat/chat_message.py +9 -9
  113. waldiez/models/chat/chat_nested.py +9 -9
  114. waldiez/models/chat/chat_summary.py +6 -6
  115. waldiez/models/common/__init__.py +2 -0
  116. waldiez/models/common/ag2_version.py +2 -0
  117. waldiez/models/common/base.py +2 -0
  118. waldiez/models/common/dict_utils.py +8 -6
  119. waldiez/models/common/handoff.py +20 -17
  120. waldiez/models/common/method_utils.py +9 -7
  121. waldiez/models/common/naming.py +49 -0
  122. waldiez/models/flow/flow.py +11 -6
  123. waldiez/models/flow/flow_data.py +23 -17
  124. waldiez/models/flow/info.py +3 -3
  125. waldiez/models/flow/naming.py +2 -1
  126. waldiez/models/model/_aws.py +11 -13
  127. waldiez/models/model/_llm.py +8 -0
  128. waldiez/models/model/_price.py +2 -4
  129. waldiez/models/model/extra_requirements.py +1 -3
  130. waldiez/models/model/model.py +2 -2
  131. waldiez/models/model/model_data.py +21 -21
  132. waldiez/models/tool/extra_requirements.py +2 -4
  133. waldiez/models/tool/predefined/_duckduckgo.py +1 -0
  134. waldiez/models/tool/predefined/_email.py +4 -0
  135. waldiez/models/tool/predefined/_google.py +1 -0
  136. waldiez/models/tool/predefined/_perplexity.py +2 -1
  137. waldiez/models/tool/predefined/_searxng.py +2 -1
  138. waldiez/models/tool/predefined/_tavily.py +1 -0
  139. waldiez/models/tool/predefined/_wikipedia.py +2 -1
  140. waldiez/models/tool/predefined/_youtube.py +1 -0
  141. waldiez/models/tool/tool.py +8 -5
  142. waldiez/models/tool/tool_data.py +2 -2
  143. waldiez/models/waldiez.py +152 -4
  144. waldiez/runner.py +11 -5
  145. waldiez/running/async_utils.py +192 -0
  146. waldiez/running/base_runner.py +155 -241
  147. waldiez/running/dir_utils.py +52 -0
  148. waldiez/running/environment.py +10 -44
  149. waldiez/running/events_mixin.py +252 -0
  150. waldiez/running/exceptions.py +20 -0
  151. waldiez/running/gen_seq_diagram.py +18 -15
  152. waldiez/running/io_utils.py +216 -0
  153. waldiez/running/protocol.py +11 -5
  154. waldiez/running/requirements_mixin.py +65 -0
  155. waldiez/running/results_mixin.py +926 -0
  156. waldiez/running/standard_runner.py +24 -27
  157. waldiez/running/step_by_step/breakpoints_mixin.py +503 -47
  158. waldiez/running/step_by_step/command_handler.py +154 -0
  159. waldiez/running/step_by_step/events_processor.py +379 -0
  160. waldiez/running/step_by_step/step_by_step_models.py +425 -41
  161. waldiez/running/step_by_step/step_by_step_runner.py +437 -382
  162. waldiez/running/subprocess_runner/__base__.py +13 -8
  163. waldiez/running/subprocess_runner/_async_runner.py +6 -4
  164. waldiez/running/subprocess_runner/_sync_runner.py +11 -6
  165. waldiez/running/subprocess_runner/runner.py +48 -23
  166. waldiez/running/timeline_processor.py +1 -1
  167. waldiez/utils/__init__.py +2 -0
  168. waldiez/utils/conflict_checker.py +4 -4
  169. waldiez/utils/python_manager.py +415 -0
  170. waldiez/ws/__init__.py +8 -7
  171. waldiez/ws/_file_handler.py +18 -20
  172. waldiez/ws/_mock.py +75 -0
  173. waldiez/ws/cli.py +58 -10
  174. waldiez/ws/client_manager.py +77 -53
  175. waldiez/ws/errors.py +3 -0
  176. waldiez/ws/models.py +61 -53
  177. waldiez/ws/reloader.py +33 -4
  178. waldiez/ws/server.py +121 -52
  179. waldiez/ws/session_manager.py +8 -9
  180. waldiez/ws/session_stats.py +1 -1
  181. waldiez/ws/utils.py +33 -5
  182. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/METADATA +107 -109
  183. waldiez-0.6.1.dist-info/RECORD +254 -0
  184. waldiez/running/post_run.py +0 -180
  185. waldiez/running/pre_run.py +0 -159
  186. waldiez/running/run_results.py +0 -14
  187. waldiez/running/utils.py +0 -511
  188. waldiez-0.5.10.dist-info/RECORD +0 -248
  189. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/WHEEL +0 -0
  190. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/entry_points.txt +0 -0
  191. {waldiez-0.5.10.dist-info → waldiez-0.6.1.dist-info}/licenses/LICENSE +0 -0
  192. {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)