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
@@ -0,0 +1,199 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Files related request handler."""
5
+
6
+ import json
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from waldiez.exporter import WaldiezExporter
12
+ from waldiez.models import Waldiez
13
+
14
+ from .models import (
15
+ ConvertWorkflowRequest,
16
+ ConvertWorkflowResponse,
17
+ SaveFlowRequest,
18
+ SaveFlowResponse,
19
+ )
20
+
21
+
22
+ class FileRequestHandler:
23
+ """Handles file-related requests."""
24
+
25
+ @staticmethod
26
+ def handle_save_flow_request(
27
+ msg: SaveFlowRequest,
28
+ workspace_dir: Path,
29
+ client_id: str,
30
+ logger: logging.Logger,
31
+ ) -> dict[str, Any]:
32
+ """Handle save flow request.
33
+
34
+ Parameters
35
+ ----------
36
+ msg : SaveFlowRequest
37
+ The save flow request message.
38
+ workspace_dir : Path
39
+ The workspace directory.
40
+ client_id : str
41
+ The client ID.
42
+ logger : logging.Logger
43
+ The logger instance.
44
+
45
+ Returns
46
+ -------
47
+ dict[str, Any]
48
+ The response dictionary.
49
+ """
50
+ filename = msg.filename or f"waldiez_{client_id}.waldiez"
51
+ try:
52
+ output_path = resolve_output_path(
53
+ filename,
54
+ workspace_dir=workspace_dir,
55
+ expected_ext="waldiez",
56
+ )
57
+ except ValueError as exc:
58
+ logger.error("Error resolving output path: %s", exc)
59
+ return SaveFlowResponse.fail(
60
+ error=f"Invalid output path: {exc}",
61
+ file_path=filename,
62
+ ).model_dump(mode="json")
63
+ # pylint: disable=too-many-try-statements
64
+ try:
65
+ if output_path.exists() and not msg.force_overwrite:
66
+ return SaveFlowResponse.fail(
67
+ error=f"File exists: {output_path}",
68
+ file_path=str(output_path.relative_to(workspace_dir)),
69
+ ).model_dump(mode="json")
70
+
71
+ # Parent dir already created by resolve_output_path
72
+ output_path.write_text(msg.flow_data, encoding="utf-8")
73
+
74
+ return SaveFlowResponse.ok(
75
+ file_path=str(output_path.relative_to(workspace_dir))
76
+ ).model_dump(mode="json")
77
+ except Exception as e: # pylint: disable=broad-exception-caught
78
+ logger.error("Error saving flow: %s", e)
79
+ return SaveFlowResponse.fail(error=str(e)).model_dump(mode="json")
80
+
81
+ @staticmethod
82
+ def handle_convert_workflow_request(
83
+ msg: ConvertWorkflowRequest,
84
+ client_id: str,
85
+ workspace_dir: Path,
86
+ logger: logging.Logger,
87
+ ) -> dict[str, Any]:
88
+ """Handle a convert workflow request.
89
+
90
+ Parameters
91
+ ----------
92
+ msg : ConvertWorkflowRequest
93
+ The convert workflow request message.
94
+ client_id : str
95
+ The client ID.
96
+ workspace_dir : Path
97
+ The workspace directory.
98
+ logger : logging.Logger
99
+ The logger instance.
100
+
101
+ Returns
102
+ -------
103
+ dict[str, Any]
104
+ The response dictionary.
105
+ """
106
+ target_format = (msg.target_format or "").strip().lower()
107
+ if target_format not in {"py", "ipynb"}:
108
+ return ConvertWorkflowResponse.fail(
109
+ error=f"Unsupported target format: {target_format}",
110
+ target_format=target_format,
111
+ ).model_dump(mode="json")
112
+
113
+ try:
114
+ waldiez_data = Waldiez.from_dict(json.loads(msg.flow_data))
115
+ except Exception as e: # pylint: disable=broad-exception-caught
116
+ return ConvertWorkflowResponse.fail(
117
+ error=f"Invalid flow_data: {e}",
118
+ target_format=target_format,
119
+ ).model_dump(mode="json")
120
+
121
+ try:
122
+ # Use normalized target_format for default name
123
+ filename = msg.output_path or f"waldiez_{client_id}.{target_format}"
124
+ output_path = resolve_output_path(
125
+ filename,
126
+ workspace_dir=workspace_dir,
127
+ expected_ext=target_format,
128
+ )
129
+ except ValueError as exc:
130
+ logger.error("Error resolving output path: %s", exc)
131
+ return ConvertWorkflowResponse.fail(
132
+ error=f"Invalid output path: {exc}",
133
+ target_format=target_format,
134
+ ).model_dump(mode="json")
135
+
136
+ try:
137
+ exporter = WaldiezExporter(waldiez_data)
138
+ exporter.export(path=output_path, force=True, structured_io=True)
139
+
140
+ return ConvertWorkflowResponse.ok(
141
+ target_format=target_format,
142
+ output_path=str(output_path.relative_to(workspace_dir)),
143
+ ).model_dump(mode="json")
144
+ except Exception as e: # pylint: disable=broad-exception-caught
145
+ logger.error("Error converting workflow: %s", e)
146
+ return ConvertWorkflowResponse.fail(
147
+ error=str(e), target_format=target_format
148
+ ).model_dump(mode="json")
149
+
150
+
151
+ def resolve_output_path(
152
+ filename: str,
153
+ workspace_dir: Path,
154
+ expected_ext: str | None = None,
155
+ ) -> Path:
156
+ """
157
+ Resolve output path inside the workspace.
158
+
159
+ Parameters
160
+ ----------
161
+ filename : str
162
+ Provided filename (may be relative or absolute).
163
+ workspace_dir : Path
164
+ The workspace directory to resolve the output path against.
165
+ expected_ext : str | None
166
+ If provided, ensure the filename ends with this extension.
167
+
168
+ Returns
169
+ -------
170
+ Path
171
+ Resolved absolute path, with parent directories created.
172
+
173
+ Raises
174
+ ------
175
+ ValueError
176
+ If the output path is outside the workspace.
177
+ """
178
+ # Normalize workspace_dir to an absolute path
179
+ workspace_dir = workspace_dir.resolve()
180
+
181
+ output_path = Path(filename)
182
+ if not output_path.is_absolute():
183
+ output_path = workspace_dir / output_path
184
+
185
+ if expected_ext and output_path.suffix != f".{expected_ext}":
186
+ output_path = output_path.with_suffix(f".{expected_ext}")
187
+
188
+ output_path = output_path.resolve()
189
+
190
+ # Ensure output_path is a subpath of workspace_dir
191
+ try:
192
+ output_path.relative_to(workspace_dir)
193
+ except ValueError as exc:
194
+ raise ValueError(
195
+ f"Output path {output_path} is outside workspace {workspace_dir}"
196
+ ) from exc
197
+
198
+ output_path.parent.mkdir(parents=True, exist_ok=True)
199
+ return output_path
waldiez/ws/_mock.py ADDED
@@ -0,0 +1,74 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+
4
+ """Mock websockets for linters."""
5
+ # pylint: disable=invalid-name,line-too-long,unused-argument,too-few-public-methods,no-self-use
6
+ # pylint: disable=missing-class-docstring,missing-function-docstring,missing-return-doc
7
+ # flake8: noqa: E501, D101, D102, D106
8
+
9
+ from typing import Any # pragma: no cover
10
+
11
+
12
+ # noinspection PyPep8Naming, PyMethodMayBeStatic,PyUnusedLocal
13
+ class websockets: # pragma: no cover
14
+ # noinspection PyMethodMayBeStatic
15
+ class ClientConnection:
16
+ async def __aenter__(self) -> "websockets.ClientConnection":
17
+ return self
18
+
19
+ async def send(self, data: Any) -> None:
20
+ pass
21
+
22
+ async def close(self) -> None:
23
+ pass
24
+
25
+ async def recv(self) -> str:
26
+ return ""
27
+
28
+ # noinspection PyPep8Naming
29
+ class exceptions:
30
+ class ConnectionClosedError(Exception): ...
31
+
32
+ class ConnectionClosedOK(Exception): ...
33
+
34
+ class Server:
35
+ def close(self) -> None:
36
+ pass
37
+
38
+ async def wait_closed(self) -> None:
39
+ pass
40
+
41
+ class ServerConnection:
42
+ remote_address: str
43
+
44
+ # noinspection PyPep8Naming
45
+ class request:
46
+ headers: dict[str, str]
47
+
48
+ async def close(
49
+ self, code: int = 1000, reason: str = "Normal Closure"
50
+ ) -> None:
51
+ pass
52
+
53
+ # noinspection PyPep8Naming
54
+ async def ConnectionClosedOK(self, *args: Any, **kwargs: Any) -> None:
55
+ pass
56
+
57
+ async def send(self, data: Any) -> None:
58
+ pass
59
+
60
+ @staticmethod
61
+ async def serve(*args: Any, **kwargs: Any) -> "websockets.Server":
62
+ return websockets.Server()
63
+
64
+ @staticmethod
65
+ async def connect(
66
+ *args: Any, **kwargs: Any
67
+ ) -> "websockets.ClientConnection":
68
+ return websockets.ClientConnection()
69
+
70
+ class ConnectionClosed(Exception):
71
+ pass
72
+
73
+ class WebSocketException(Exception):
74
+ pass
waldiez/ws/cli.py ADDED
@@ -0,0 +1,235 @@
1
+ # SPDX-License-Identifier: Apache-2.0.
2
+ # Copyright (c) 2024 - 2025 Waldiez and contributors.
3
+ # pylint: disable=too-many-locals,unused-import
4
+ """CLI interface for Waldiez WebSocket server."""
5
+
6
+ import asyncio
7
+ import logging
8
+ import re
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Annotated, Any, Optional, Set
12
+
13
+ import typer
14
+
15
+ HAS_WATCHDOG = False
16
+ try:
17
+ from .reloader import FileWatcher # pyright: ignore # noqa: F401
18
+
19
+ HAS_WATCHDOG = True # pyright: ignore
20
+ except ImportError:
21
+ pass
22
+
23
+ HAS_WEBSOCKETS = False
24
+ try:
25
+ from .server import run_server
26
+
27
+ HAS_WEBSOCKETS = True # pyright: ignore
28
+ except ImportError:
29
+ # pylint: disable=missing-param-doc,missing-raises-doc
30
+ # noinspection PyUnusedLocal
31
+ async def run_server(*args: Any, **kwargs: Any) -> None: # type: ignore
32
+ """No WebSocket server available."""
33
+ raise NotImplementedError("WebSocket server is not available.")
34
+
35
+
36
+ DEFAULT_WORKSPACE_DIR = Path.cwd()
37
+
38
+
39
+ def setup_logging(verbose: bool = False) -> None:
40
+ """Set up logging configuration.
41
+
42
+ Parameters
43
+ ----------
44
+ verbose : bool
45
+ Enable verbose logging
46
+ """
47
+ level = logging.DEBUG if verbose else logging.INFO
48
+ format_str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
49
+
50
+ logging.basicConfig(
51
+ level=level,
52
+ format=format_str,
53
+ handlers=[logging.StreamHandler(sys.stdout)],
54
+ )
55
+
56
+ # Set websockets logger to WARNING to reduce noise
57
+ logging.getLogger("websockets").setLevel(logging.WARNING)
58
+
59
+
60
+ app = typer.Typer(
61
+ name="waldiez-ws",
62
+ help="Waldiez WebSocket server",
63
+ add_completion=False,
64
+ context_settings={
65
+ "help_option_names": ["-h", "--help"],
66
+ "allow_extra_args": True,
67
+ "ignore_unknown_options": True,
68
+ },
69
+ )
70
+
71
+
72
+ @app.command()
73
+ def serve(
74
+ host: Annotated[
75
+ str, typer.Option(help="Server host address")
76
+ ] = "localhost",
77
+ port: Annotated[int, typer.Option(help="Server port")] = 8765,
78
+ max_clients: Annotated[
79
+ int,
80
+ typer.Option(
81
+ "--max-clients", help="Maximum number of concurrent clients"
82
+ ),
83
+ ] = 1,
84
+ allowed_origins: Annotated[
85
+ Optional[list[str]],
86
+ typer.Option(
87
+ "--allowed-origin",
88
+ help=(
89
+ "Allowed origins for CORS (can be used multiple times). "
90
+ "Supports regex patterns. "
91
+ "Examples: 'https://example.com', '.*\\.mydomain\\.com'"
92
+ ),
93
+ ),
94
+ ] = None,
95
+ auto_reload: Annotated[
96
+ bool,
97
+ typer.Option(
98
+ "--auto-reload",
99
+ help=(
100
+ "Enable auto-reload on file changes "
101
+ "(requires: pip install watchdog)"
102
+ ),
103
+ ),
104
+ ] = False,
105
+ watch_dir: Annotated[
106
+ Optional[list[Path]],
107
+ typer.Option(
108
+ "--watch-dir",
109
+ help=(
110
+ "Additional directories to watch for auto-reload "
111
+ "(can be used multiple times)"
112
+ ),
113
+ ),
114
+ ] = None,
115
+ workspace_dir: Annotated[
116
+ Path,
117
+ typer.Option(
118
+ "--workspace",
119
+ help="Path to the workspace directory",
120
+ resolve_path=True,
121
+ dir_okay=True,
122
+ file_okay=False,
123
+ ),
124
+ ] = DEFAULT_WORKSPACE_DIR,
125
+ ping_interval: Annotated[
126
+ float,
127
+ typer.Option(
128
+ "--ping-interval", help="WebSocket ping interval in seconds"
129
+ ),
130
+ ] = 20.0,
131
+ ping_timeout: Annotated[
132
+ float,
133
+ typer.Option(
134
+ "--ping-timeout", help="WebSocket ping timeout in seconds"
135
+ ),
136
+ ] = 20.0,
137
+ max_size: Annotated[
138
+ int, typer.Option("--max-size", help="Maximum message size in bytes")
139
+ ] = 8388608,
140
+ verbose: Annotated[
141
+ bool, typer.Option("--verbose", "-v", help="Enable verbose logging")
142
+ ] = False,
143
+ ) -> None:
144
+ """Start Waldiez WebSocket server.
145
+
146
+ Parameters
147
+ ----------
148
+ host : str
149
+ Server host address
150
+ port : int
151
+ Server port
152
+ max_clients : int
153
+ Maximum number of concurrent clients
154
+ allowed_origins : list[str] | None
155
+ List of allowed origins for CORS (default: None)
156
+ auto_reload : bool
157
+ Enable auto-reload on file changes
158
+ watch_dir : tuple[Path, ...]
159
+ Additional directories to watch for auto-reload
160
+ workspace_dir : Path
161
+ Path to the workspace directory
162
+ ping_interval : float
163
+ WebSocket ping interval in seconds
164
+ ping_timeout : float
165
+ WebSocket ping timeout in seconds
166
+ max_size : int
167
+ Maximum message size in bytes
168
+ verbose : bool
169
+ Enable verbose logging
170
+ """
171
+ setup_logging(verbose)
172
+
173
+ logger = logging.getLogger(__name__)
174
+
175
+ # Convert watch directories to set
176
+ watch_dirs: Optional[Set[Path]] = None
177
+ if watch_dir:
178
+ watch_dirs = set(watch_dir)
179
+
180
+ compiled_origins: list[re.Pattern[str]] | None = None
181
+ if allowed_origins:
182
+ try:
183
+ compiled_origins = [
184
+ re.compile(pattern) for pattern in allowed_origins
185
+ ]
186
+ except re.error as e:
187
+ typer.echo(f"Invalid regex pattern in allowed origins: {e}")
188
+ sys.exit(1)
189
+
190
+ # Server configuration
191
+ server_config: dict[str, Any] = {
192
+ "max_clients": max_clients,
193
+ "allowed_origins": compiled_origins,
194
+ "ping_interval": ping_interval,
195
+ "ping_timeout": ping_timeout,
196
+ "max_size": max_size,
197
+ }
198
+ if not HAS_WATCHDOG and auto_reload:
199
+ typer.echo(
200
+ "Auto-reload requires the 'watchdog' package. "
201
+ "Please install it with: pip install watchdog"
202
+ )
203
+ auto_reload = False
204
+ logger.info("Starting Waldiez WebSocket server...")
205
+ logger.info("Configuration:")
206
+ logger.info(" Host: %s", host)
207
+ logger.info(" Port: %d", port)
208
+ logger.info(" Max clients: %d", max_clients)
209
+ logger.info(" Allowed origins: %s", allowed_origins or ["*"])
210
+ logger.info(" Auto-reload: %s", auto_reload)
211
+ logger.info(" Workspace directory: %s", workspace_dir)
212
+
213
+ if watch_dirs:
214
+ logger.info(" Watch directories: %s", watch_dirs)
215
+
216
+ try:
217
+ asyncio.run(
218
+ run_server(
219
+ host=host,
220
+ port=port,
221
+ workspace_dir=workspace_dir,
222
+ auto_reload=auto_reload,
223
+ watch_dirs=watch_dirs,
224
+ **server_config,
225
+ )
226
+ )
227
+ except KeyboardInterrupt:
228
+ logger.info("Server stopped by user")
229
+ except Exception as e: # pylint: disable=broad-exception-caught
230
+ logger.error("Server error: %s", e)
231
+ sys.exit(1)
232
+
233
+
234
+ if __name__ == "__main__":
235
+ app()