waldiez 0.5.9__py3-none-any.whl → 0.6.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.

Potentially problematic release.


This version of waldiez might be problematic. Click here for more details.

Files changed (109) hide show
  1. waldiez/_version.py +1 -1
  2. waldiez/cli.py +113 -24
  3. waldiez/exporting/agent/exporter.py +9 -6
  4. waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
  5. waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
  6. waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
  7. waldiez/exporting/agent/extras/handoffs/available.py +1 -0
  8. waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
  9. waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
  10. waldiez/exporting/agent/extras/handoffs/target.py +1 -0
  11. waldiez/exporting/agent/termination.py +1 -0
  12. waldiez/exporting/chats/utils/common.py +25 -23
  13. waldiez/exporting/core/__init__.py +0 -2
  14. waldiez/exporting/core/constants.py +3 -1
  15. waldiez/exporting/core/context.py +13 -13
  16. waldiez/exporting/core/extras/serializer.py +12 -10
  17. waldiez/exporting/core/protocols.py +0 -141
  18. waldiez/exporting/core/result.py +5 -5
  19. waldiez/exporting/core/types.py +1 -0
  20. waldiez/exporting/core/utils/llm_config.py +2 -2
  21. waldiez/exporting/flow/execution_generator.py +1 -0
  22. waldiez/exporting/flow/merger.py +2 -2
  23. waldiez/exporting/flow/orchestrator.py +1 -0
  24. waldiez/exporting/flow/utils/common.py +3 -3
  25. waldiez/exporting/flow/utils/importing.py +1 -0
  26. waldiez/exporting/flow/utils/logging.py +7 -80
  27. waldiez/exporting/tools/exporter.py +5 -0
  28. waldiez/exporting/tools/factory.py +4 -0
  29. waldiez/exporting/tools/processor.py +5 -1
  30. waldiez/io/__init__.py +3 -1
  31. waldiez/io/_ws.py +15 -5
  32. waldiez/io/models/content/image.py +1 -0
  33. waldiez/io/models/user_input.py +4 -4
  34. waldiez/io/models/user_response.py +1 -0
  35. waldiez/io/mqtt.py +1 -1
  36. waldiez/io/structured.py +98 -45
  37. waldiez/io/utils.py +17 -11
  38. waldiez/io/ws.py +10 -12
  39. waldiez/logger.py +180 -63
  40. waldiez/models/agents/agent/agent.py +2 -1
  41. waldiez/models/agents/agent/update_system_message.py +0 -2
  42. waldiez/models/agents/doc_agent/doc_agent.py +8 -1
  43. waldiez/models/chat/chat.py +1 -0
  44. waldiez/models/chat/chat_data.py +0 -2
  45. waldiez/models/common/base.py +2 -0
  46. waldiez/models/common/dict_utils.py +169 -40
  47. waldiez/models/common/handoff.py +2 -0
  48. waldiez/models/common/method_utils.py +2 -0
  49. waldiez/models/flow/flow.py +6 -6
  50. waldiez/models/flow/info.py +5 -1
  51. waldiez/models/model/_llm.py +31 -14
  52. waldiez/models/model/model.py +4 -1
  53. waldiez/models/model/model_data.py +18 -5
  54. waldiez/models/tool/predefined/_config.py +5 -1
  55. waldiez/models/tool/predefined/_duckduckgo.py +4 -0
  56. waldiez/models/tool/predefined/_email.py +477 -0
  57. waldiez/models/tool/predefined/_google.py +4 -1
  58. waldiez/models/tool/predefined/_perplexity.py +4 -1
  59. waldiez/models/tool/predefined/_searxng.py +4 -1
  60. waldiez/models/tool/predefined/_tavily.py +4 -1
  61. waldiez/models/tool/predefined/_wikipedia.py +5 -2
  62. waldiez/models/tool/predefined/_youtube.py +4 -1
  63. waldiez/models/tool/predefined/protocol.py +3 -0
  64. waldiez/models/tool/tool.py +22 -4
  65. waldiez/models/waldiez.py +12 -0
  66. waldiez/runner.py +37 -54
  67. waldiez/running/__init__.py +6 -0
  68. waldiez/running/base_runner.py +381 -363
  69. waldiez/running/environment.py +1 -0
  70. waldiez/running/exceptions.py +9 -0
  71. waldiez/running/post_run.py +10 -4
  72. waldiez/running/pre_run.py +199 -66
  73. waldiez/running/protocol.py +21 -101
  74. waldiez/running/run_results.py +1 -1
  75. waldiez/running/standard_runner.py +83 -276
  76. waldiez/running/step_by_step/__init__.py +46 -0
  77. waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
  78. waldiez/running/step_by_step/command_handler.py +151 -0
  79. waldiez/running/step_by_step/events_processor.py +199 -0
  80. waldiez/running/step_by_step/step_by_step_models.py +541 -0
  81. waldiez/running/step_by_step/step_by_step_runner.py +750 -0
  82. waldiez/running/subprocess_runner/__base__.py +279 -0
  83. waldiez/running/subprocess_runner/__init__.py +16 -0
  84. waldiez/running/subprocess_runner/_async_runner.py +362 -0
  85. waldiez/running/subprocess_runner/_sync_runner.py +456 -0
  86. waldiez/running/subprocess_runner/runner.py +570 -0
  87. waldiez/running/timeline_processor.py +1 -1
  88. waldiez/running/utils.py +492 -3
  89. waldiez/utils/version.py +2 -6
  90. waldiez/ws/__init__.py +71 -0
  91. waldiez/ws/__main__.py +15 -0
  92. waldiez/ws/_file_handler.py +199 -0
  93. waldiez/ws/_mock.py +74 -0
  94. waldiez/ws/cli.py +235 -0
  95. waldiez/ws/client_manager.py +851 -0
  96. waldiez/ws/errors.py +416 -0
  97. waldiez/ws/models.py +988 -0
  98. waldiez/ws/reloader.py +363 -0
  99. waldiez/ws/server.py +508 -0
  100. waldiez/ws/session_manager.py +393 -0
  101. waldiez/ws/session_stats.py +83 -0
  102. waldiez/ws/utils.py +410 -0
  103. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
  104. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
  105. waldiez/running/patch_io_stream.py +0 -210
  106. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
  107. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
  108. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
  109. {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
waldiez/running/utils.py CHANGED
@@ -3,17 +3,44 @@
3
3
  """Common utilities for the waldiez runner."""
4
4
 
5
5
  import asyncio
6
+ import functools
7
+ import inspect
8
+ import logging
6
9
  import os
7
10
  import re
8
11
  import subprocess
9
12
  import sys
13
+ import threading
14
+ import traceback
10
15
 
11
16
  # noinspection PyProtectedMember
12
17
  from asyncio.subprocess import Process
13
18
  from contextlib import asynccontextmanager, contextmanager
14
19
  from dataclasses import dataclass
20
+ from getpass import getpass
15
21
  from pathlib import Path
16
- from typing import AsyncIterator, Iterator, Union
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
17
44
 
18
45
 
19
46
  @dataclass
@@ -70,6 +97,71 @@ async def a_chdir(to: Union[str, Path]) -> AsyncIterator[None]:
70
97
  os.chdir(old_cwd)
71
98
 
72
99
 
100
+ def get_python_executable() -> str:
101
+ """Get the appropriate Python executable path.
102
+
103
+ For bundled applications, this might be different from sys.executable.
104
+
105
+ Returns
106
+ -------
107
+ str
108
+ Path to the Python executable to use for pip operations.
109
+ """
110
+ # Check if we're in a bundled application (e.g., PyInstaller)
111
+ if getattr(sys, "frozen", False): # pragma: no cover
112
+ # We're in a bundled app
113
+ if hasattr(sys, "_MEIPASS"):
114
+ sys_meipass = getattr(
115
+ sys, "_MEIPASS", str(Path.home() / ".waldiez" / "bin")
116
+ )
117
+ bundled = Path(sys_meipass) / "python"
118
+ if bundled.exists():
119
+ return str(bundled)
120
+ return sys.executable
121
+
122
+
123
+ # noinspection TryExceptPass,PyBroadException
124
+ def ensure_pip() -> None: # pragma: no cover
125
+ """Make sure `python -m pip` works (bootstrap via ensurepip if needed)."""
126
+ # pylint: disable=import-outside-toplevel
127
+ # pylint: disable=unused-import,broad-exception-caught
128
+ try:
129
+ import pip # noqa: F401 # pyright: ignore
130
+
131
+ return
132
+ except Exception:
133
+ pass
134
+ try:
135
+ import ensurepip
136
+
137
+ ensurepip.bootstrap(upgrade=True)
138
+ except Exception:
139
+ # If bootstrap fails, we'll still attempt `-m pip` and surface errors.
140
+ pass
141
+
142
+
143
+ def get_pip_install_location() -> str | None:
144
+ """Determine the best location to install packages.
145
+
146
+ Returns
147
+ -------
148
+ Optional[str]
149
+ The installation target directory, or None for default.
150
+ """
151
+ if getattr(sys, "frozen", False): # pragma: no cover
152
+ # For bundled apps, try to install to a user-writable location
153
+ if hasattr(sys, "_MEIPASS"):
154
+ app_data = Path.home() / ".waldiez" / "site-packages"
155
+ app_data.mkdir(parents=True, exist_ok=True)
156
+ # Add to sys.path if not already there
157
+ app_data_str = str(app_data)
158
+ if app_data_str not in sys.path:
159
+ # after stdlib
160
+ sys.path.insert(1, app_data_str)
161
+ return app_data_str
162
+ return None
163
+
164
+
73
165
  def strip_ansi(text: str) -> str:
74
166
  """Remove ANSI escape sequences from text.
75
167
 
@@ -101,7 +193,7 @@ def create_sync_subprocess(setup: ProcessSetup) -> subprocess.Popen[bytes]:
101
193
  The created subprocess.
102
194
  """
103
195
  return subprocess.Popen(
104
- [sys.executable, "-u", str(setup.file_path)],
196
+ [get_python_executable(), "-u", str(setup.file_path)],
105
197
  stdout=subprocess.PIPE,
106
198
  stderr=subprocess.PIPE,
107
199
  stdin=subprocess.PIPE,
@@ -126,7 +218,7 @@ async def create_async_subprocess(setup: ProcessSetup) -> Process:
126
218
  The created asynchronous subprocess.
127
219
  """
128
220
  return await asyncio.create_subprocess_exec(
129
- sys.executable,
221
+ get_python_executable(),
130
222
  "-u",
131
223
  str(setup.file_path),
132
224
  # stdout=asyncio.subprocess.PIPE,
@@ -134,3 +226,400 @@ async def create_async_subprocess(setup: ProcessSetup) -> Process:
134
226
  # stdin=asyncio.subprocess.PIPE,
135
227
  env={**os.environ},
136
228
  )
229
+
230
+
231
+ async def input_async(prompt: str, *, password: bool = False) -> str:
232
+ """Asynchronous input function.
233
+
234
+ Parameters
235
+ ----------
236
+ prompt : str
237
+ The prompt to display to the user.
238
+ password : bool, optional
239
+ Whether to hide input (password mode), by default False.
240
+
241
+ Returns
242
+ -------
243
+ str
244
+ The user input.
245
+ """
246
+ if password:
247
+ try:
248
+ return await asyncio.to_thread(getpass, prompt)
249
+ except EOFError:
250
+ return ""
251
+ try:
252
+ return await asyncio.to_thread(input, prompt)
253
+ except EOFError:
254
+ return ""
255
+
256
+
257
+ def input_sync(prompt: str, *, password: bool = False) -> str:
258
+ """Input function (synchronous).
259
+
260
+ Parameters
261
+ ----------
262
+ prompt : str
263
+ The prompt to display to the user.
264
+ password : bool, optional
265
+ Whether to hide input (password mode), by default False.
266
+
267
+ Returns
268
+ -------
269
+ str
270
+ The user input.
271
+ """
272
+ if password:
273
+ try:
274
+ return getpass(prompt)
275
+ except EOFError:
276
+ return ""
277
+ try:
278
+ return input(prompt)
279
+ except EOFError:
280
+ return ""
281
+
282
+
283
+ # pylint: disable=import-outside-toplevel,too-complex
284
+ def get_printer() -> Callable[..., None]: # noqa: C901
285
+ """Get the printer function.
286
+
287
+ Returns
288
+ -------
289
+ Callable[..., None]
290
+ The printer function that handles Unicode encoding errors gracefully.
291
+ """
292
+ try:
293
+ # noinspection PyUnresolvedReferences
294
+ from autogen.io import IOStream # type: ignore
295
+
296
+ printer = IOStream.get_default().print
297
+ except ImportError: # pragma: no cover
298
+ # Fallback to standard print if autogen is not available
299
+ printer = print
300
+
301
+ # noinspection PyBroadException,TryExceptPass
302
+ def safe_printer(*args: Any, **kwargs: Any) -> None: # noqa: C901
303
+ """Safe printer that handles Unicode encoding errors.
304
+
305
+ Parameters
306
+ ----------
307
+ *args : Any
308
+ Arguments to pass to the printer
309
+ **kwargs : Any
310
+ Keyword arguments to pass to the printer
311
+ """
312
+ # pylint: disable=broad-exception-caught,too-many-try-statements
313
+ try:
314
+ printer(*args, **kwargs)
315
+ except (UnicodeEncodeError, UnicodeDecodeError):
316
+ # First fallback: try to get a safe string representation
317
+ try:
318
+ msg, flush = get_what_to_print(*args, **kwargs)
319
+ # Convert problematic characters to safe representations
320
+ safe_msg = msg.encode("utf-8", errors="replace").decode("utf-8")
321
+ printer(safe_msg, end="", flush=flush)
322
+ except (UnicodeEncodeError, UnicodeDecodeError):
323
+ # Second fallback: use built-in print with safe encoding
324
+ try:
325
+ # Convert args to safe string representations
326
+ safe_args: list[str] = []
327
+ for arg in args:
328
+ try:
329
+ safe_args.append(
330
+ str(arg)
331
+ .encode("utf-8", errors="replace")
332
+ .decode("utf-8")
333
+ )
334
+ except (
335
+ UnicodeEncodeError,
336
+ UnicodeDecodeError,
337
+ ): # pragma: no cover
338
+ safe_args.append(repr(arg))
339
+
340
+ # Use built-in print instead of the custom printer
341
+ print(*safe_args, **kwargs)
342
+
343
+ except Exception:
344
+ # Final fallback: write directly to stderr buffer
345
+ error_msg = (
346
+ "Could not print the message due to encoding issues.\n"
347
+ )
348
+ to_sys_stderr(error_msg)
349
+ except Exception as e:
350
+ # Handle any other unexpected errors
351
+ traceback.print_exc()
352
+ error_msg = f"Unexpected error in printer: {str(e)}\n"
353
+ to_sys_stderr(error_msg)
354
+
355
+ return safe_printer
356
+
357
+
358
+ def to_sys_stderr(msg: str) -> None:
359
+ """Write a message to sys.stderr.
360
+
361
+ Parameters
362
+ ----------
363
+ msg : str
364
+ The message to write to stderr.
365
+ """
366
+ # pylint: disable=broad-exception-caught
367
+ # noinspection TryExceptPass,PyBroadException
368
+ try:
369
+ if hasattr(sys.stderr, "buffer"):
370
+ sys.stderr.buffer.write(msg.encode("utf-8", errors="replace"))
371
+ sys.stderr.buffer.flush()
372
+ else: # pragma: no cover
373
+ sys.stderr.write(msg)
374
+ sys.stderr.flush()
375
+ except Exception: # pragma: no cover
376
+ pass
377
+
378
+
379
+ def get_what_to_print(*args: Any, **kwargs: Any) -> tuple[str, bool]:
380
+ """Extract message and flush flag from print arguments.
381
+
382
+ Parameters
383
+ ----------
384
+ *args : Any
385
+ Arguments to print
386
+ **kwargs : Any
387
+ Keyword arguments for print function
388
+
389
+ Returns
390
+ -------
391
+ tuple[str, bool]
392
+ Message to print and flush flag
393
+ """
394
+ # Convert all args to strings and join with spaces (like print does)
395
+ msg = " ".join(str(arg) for arg in args)
396
+
397
+ # Handle sep parameter
398
+ sep = kwargs.get("sep", " ")
399
+ if isinstance(sep, bytes): # pragma: no cover
400
+ sep = sep.decode("utf-8", errors="replace")
401
+ if len(args) > 1:
402
+ msg = sep.join(str(arg) for arg in args)
403
+
404
+ # Handle end parameter
405
+ end = kwargs.get("end", "\n")
406
+ if isinstance(end, bytes): # pragma: no cover
407
+ end = end.decode("utf-8", errors="replace")
408
+ msg += end
409
+
410
+ # Handle flush parameter
411
+ flush = kwargs.get("flush", False)
412
+ # noinspection PyUnreachableCode
413
+ if not isinstance(flush, bool): # pragma: no cover
414
+ flush = False
415
+
416
+ return msg, flush
417
+
418
+
419
+ def is_async_callable(fn: Any) -> bool:
420
+ """Check if a function is async callable, including partials/callables.
421
+
422
+ Parameters
423
+ ----------
424
+ fn : Any
425
+ The function to check.
426
+
427
+ Returns
428
+ -------
429
+ bool
430
+ True if the function is async callable, False otherwise.
431
+ """
432
+ if isinstance(fn, functools.partial):
433
+ fn = fn.func
434
+ unwrapped = inspect.unwrap(fn)
435
+ return inspect.iscoroutinefunction(
436
+ unwrapped
437
+ ) or inspect.iscoroutinefunction(
438
+ getattr(unwrapped, "__call__", None), # noqa: B004
439
+ )
440
+
441
+
442
+ def syncify(
443
+ async_func: Callable[..., Coroutine[Any, Any, T]],
444
+ timeout: float | None = None,
445
+ ) -> Callable[..., T]:
446
+ """Convert an async function to a sync function.
447
+
448
+ This function handles the conversion of async functions to sync functions,
449
+ properly managing event loops and thread execution contexts.
450
+
451
+ Parameters
452
+ ----------
453
+ async_func : Callable[..., Coroutine[Any, Any, T]]
454
+ The async function to convert.
455
+ timeout : float | None, optional
456
+ The timeout for the sync function. Defaults to None.
457
+
458
+ Returns
459
+ -------
460
+ Callable[..., T]
461
+ The converted sync function.
462
+
463
+ Raises
464
+ ------
465
+ TimeoutError
466
+ If the async function times out.
467
+ RuntimeError
468
+ If there are issues with event loop management.
469
+ """
470
+
471
+ def _sync_wrapper(*args: Any, **kwargs: Any) -> T:
472
+ """Get the result of the async function."""
473
+ # pylint: disable=too-many-try-statements
474
+ try:
475
+ # Check if we're already in an event loop
476
+ asyncio.get_running_loop()
477
+ return _run_in_thread(async_func, args, kwargs, timeout)
478
+ except RuntimeError:
479
+ # No event loop running, we can use asyncio.run directly
480
+ logger.debug("No event loop running, using asyncio.run")
481
+
482
+ # Create a new event loop and run the coroutine
483
+ try:
484
+ if timeout is not None:
485
+ # Need to run with asyncio.run and wait_for inside
486
+ async def _with_timeout() -> T:
487
+ return await asyncio.wait_for(
488
+ async_func(*args, **kwargs), timeout=timeout
489
+ )
490
+
491
+ return asyncio.run(_with_timeout())
492
+ return asyncio.run(async_func(*args, **kwargs))
493
+ except (asyncio.TimeoutError, TimeoutError) as e:
494
+ raise TimeoutError(
495
+ f"Async function timed out after {timeout} seconds"
496
+ ) from e
497
+
498
+ return _sync_wrapper
499
+
500
+
501
+ def _run_in_thread(
502
+ async_func: Callable[..., Coroutine[Any, Any, T]],
503
+ args: tuple[Any, ...],
504
+ kwargs: dict[str, Any],
505
+ timeout: float | None,
506
+ ) -> T:
507
+ """Run async function in a separate thread.
508
+
509
+ Parameters
510
+ ----------
511
+ async_func : Callable[..., Coroutine[Any, Any, T]]
512
+ The async function to run.
513
+ args : tuple[Any, ...]
514
+ Positional arguments for the function.
515
+ kwargs : dict[str, Any]
516
+ Keyword arguments for the function.
517
+ timeout : float | None
518
+ Timeout in seconds.
519
+
520
+ Returns
521
+ -------
522
+ T
523
+ The result of the async function.
524
+
525
+ Raises
526
+ ------
527
+ TimeoutError
528
+ If the function execution times out.
529
+ RuntimeError
530
+ If thread execution fails unexpectedly.
531
+ """
532
+ result_container: _ResultContainer[T] = _ResultContainer()
533
+ finished_event = threading.Event()
534
+
535
+ def _thread_target() -> None:
536
+ """Target function for the thread."""
537
+ # pylint: disable=too-many-try-statements, broad-exception-caught
538
+ try:
539
+ if timeout is not None:
540
+
541
+ async def _with_timeout() -> T:
542
+ return await asyncio.wait_for(
543
+ async_func(*args, **kwargs), timeout=timeout
544
+ )
545
+
546
+ result_container.result = asyncio.run(_with_timeout())
547
+ else:
548
+ result_container.result = asyncio.run(
549
+ async_func(*args, **kwargs)
550
+ )
551
+ except (
552
+ BaseException
553
+ ) as e: # Catch BaseException to propagate cancellations
554
+ result_container.exception = e
555
+ finally:
556
+ finished_event.set()
557
+
558
+ thread = threading.Thread(target=_thread_target, daemon=True)
559
+ thread.start()
560
+
561
+ # Wait for completion with timeout
562
+ timeout_buffer = 1.0 # 1 second buffer for cleanup
563
+ wait_timeout = timeout + timeout_buffer if timeout is not None else None
564
+
565
+ if not finished_event.wait(timeout=wait_timeout): # pragma: no cover
566
+ raise TimeoutError(
567
+ f"Function execution timed out after {timeout} seconds"
568
+ )
569
+
570
+ thread.join(timeout=timeout_buffer) # Give thread time to clean up
571
+
572
+ if result_container.exception is not None:
573
+ raise result_container.exception
574
+
575
+ # Use cast since we know the result should be T if no exception occurred
576
+ return cast(T, result_container.result)
577
+
578
+
579
+ def safe_filename(name: str, ext: str = "") -> str:
580
+ """
581
+ Make a safe cross-platform filename from an arbitrary string.
582
+
583
+ Parameters
584
+ ----------
585
+ name : str
586
+ The string to turn into a safe filename.
587
+ ext : str
588
+ Optional extension (with or without leading dot).
589
+
590
+ Returns
591
+ -------
592
+ str
593
+ A safe filename string.
594
+ """
595
+ # Normalize extension
596
+ # pylint: disable=inconsistent-quotes
597
+ ext = f".{ext.lstrip('.')}" if ext else ""
598
+
599
+ # Forbidden characters on Windows (also bad on Unix)
600
+ forbidden = r'[<>:"/\\|?*\x00-\x1F]'
601
+ name = re.sub(forbidden, "_", name)
602
+
603
+ # Trim trailing dots/spaces (illegal on Windows)
604
+ name = name.rstrip(". ")
605
+
606
+ # Collapse multiple underscores
607
+ name = re.sub(r"_+", "_", name)
608
+
609
+ # Reserved Windows device names
610
+ reserved = re.compile(
611
+ r"^(con|prn|aux|nul|com[1-9]|lpt[1-9])$", re.IGNORECASE
612
+ )
613
+ if reserved.match(name):
614
+ name = f"_{name}"
615
+
616
+ # Fallback if empty
617
+ if not name:
618
+ name = "file"
619
+
620
+ # Ensure length limit (NTFS max filename length = 255 bytes)
621
+ max_len = 255 - len(ext)
622
+ if len(name) > max_len:
623
+ name = name[:max_len]
624
+
625
+ return f"{name}{ext}"
waldiez/utils/version.py CHANGED
@@ -30,7 +30,7 @@ def _get_waldiez_version_from_package_json() -> str | None:
30
30
  def _get_waldiez_version_from_version_py() -> str | None:
31
31
  """Get the Waldiez version from _version.py."""
32
32
  version_py_path = Path(__file__).parent.parent / "_version.py"
33
- if version_py_path.exists():
33
+ if version_py_path.exists(): # pragma: no branch
34
34
  with open(version_py_path, "r", encoding="utf-8") as f:
35
35
  for line in f:
36
36
  if line.startswith("__version__"):
@@ -48,14 +48,10 @@ def get_waldiez_version() -> str:
48
48
  The Waldiez version, or "dev" if not found.
49
49
  """
50
50
  w_version = _get_waldiez_version_from_importlib()
51
- if not w_version:
51
+ if not w_version: # pragma: no branch
52
52
  w_version = _get_waldiez_version_from_version_py()
53
53
  if not w_version:
54
54
  w_version = _get_waldiez_version_from_package_json()
55
55
  if not w_version:
56
56
  w_version = "dev"
57
57
  return w_version
58
-
59
-
60
- if __name__ == "__main__":
61
- print(get_waldiez_version())
waldiez/ws/__init__.py ADDED
@@ -0,0 +1,71 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=duplicate-code
4
+ """Waldiez WebSocket server module."""
5
+
6
+ import typer
7
+ from typer.models import CommandInfo
8
+
9
+ from .cli import serve
10
+ from .client_manager import ClientManager
11
+ from .errors import (
12
+ ErrorCode,
13
+ ErrorHandler,
14
+ MessageHandlingError,
15
+ MessageParsingError,
16
+ OperationTimeoutError,
17
+ ServerOverloadError,
18
+ UnsupportedActionError,
19
+ WaldiezServerError,
20
+ )
21
+ from .server import HAS_WEBSOCKETS, WaldiezWsServer, run_server
22
+ from .session_manager import SessionManager
23
+ from .utils import (
24
+ ConnectionManager,
25
+ HealthChecker,
26
+ ServerHealth,
27
+ get_available_port,
28
+ is_port_available,
29
+ test_server_connection,
30
+ )
31
+
32
+
33
+ def add_ws_app(app: typer.Typer) -> None:
34
+ """Add WebSocket server commands to the CLI.
35
+
36
+ Parameters
37
+ ----------
38
+ app : typer.Typer
39
+ The Typer application instance.
40
+ """
41
+ if HAS_WEBSOCKETS:
42
+ app.registered_commands.append(
43
+ CommandInfo(
44
+ name="ws",
45
+ help="Start the Waldiez WebSocket server.",
46
+ callback=serve,
47
+ )
48
+ )
49
+
50
+
51
+ __all__ = [
52
+ "WaldiezWsServer",
53
+ "run_server",
54
+ "ClientManager",
55
+ "ConnectionManager",
56
+ "HealthChecker",
57
+ "ServerHealth",
58
+ "test_server_connection",
59
+ "ErrorHandler",
60
+ "ErrorCode",
61
+ "MessageParsingError",
62
+ "MessageHandlingError",
63
+ "UnsupportedActionError",
64
+ "ServerOverloadError",
65
+ "SessionManager",
66
+ "OperationTimeoutError",
67
+ "WaldiezServerError",
68
+ "get_available_port",
69
+ "is_port_available",
70
+ "add_ws_app",
71
+ ]
waldiez/ws/__main__.py ADDED
@@ -0,0 +1,15 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ """Main entry point for Waldiez WebSocket server."""
4
+
5
+ import sys # pragma: no cover
6
+ from pathlib import Path # pragma: no cover
7
+
8
+ try: # pragma: no cover
9
+ from .cli import app
10
+ except ImportError: # pragma: no cover
11
+ sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
12
+ from waldiez.ws.cli import app
13
+
14
+ if __name__ == "__main__":
15
+ app()