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.
- waldiez/_version.py +1 -1
- waldiez/cli.py +113 -24
- waldiez/exporting/agent/exporter.py +9 -6
- waldiez/exporting/agent/extras/captain_agent_extras.py +44 -7
- waldiez/exporting/agent/extras/group_manager_agent_extas.py +6 -1
- waldiez/exporting/agent/extras/handoffs/after_work.py +1 -0
- waldiez/exporting/agent/extras/handoffs/available.py +1 -0
- waldiez/exporting/agent/extras/handoffs/condition.py +3 -1
- waldiez/exporting/agent/extras/handoffs/handoff.py +1 -0
- waldiez/exporting/agent/extras/handoffs/target.py +1 -0
- waldiez/exporting/agent/termination.py +1 -0
- waldiez/exporting/chats/utils/common.py +25 -23
- waldiez/exporting/core/__init__.py +0 -2
- waldiez/exporting/core/constants.py +3 -1
- waldiez/exporting/core/context.py +13 -13
- waldiez/exporting/core/extras/serializer.py +12 -10
- waldiez/exporting/core/protocols.py +0 -141
- waldiez/exporting/core/result.py +5 -5
- waldiez/exporting/core/types.py +1 -0
- waldiez/exporting/core/utils/llm_config.py +2 -2
- waldiez/exporting/flow/execution_generator.py +1 -0
- waldiez/exporting/flow/merger.py +2 -2
- waldiez/exporting/flow/orchestrator.py +1 -0
- waldiez/exporting/flow/utils/common.py +3 -3
- waldiez/exporting/flow/utils/importing.py +1 -0
- waldiez/exporting/flow/utils/logging.py +7 -80
- waldiez/exporting/tools/exporter.py +5 -0
- waldiez/exporting/tools/factory.py +4 -0
- waldiez/exporting/tools/processor.py +5 -1
- waldiez/io/__init__.py +3 -1
- waldiez/io/_ws.py +15 -5
- waldiez/io/models/content/image.py +1 -0
- waldiez/io/models/user_input.py +4 -4
- waldiez/io/models/user_response.py +1 -0
- waldiez/io/mqtt.py +1 -1
- waldiez/io/structured.py +98 -45
- waldiez/io/utils.py +17 -11
- waldiez/io/ws.py +10 -12
- waldiez/logger.py +180 -63
- waldiez/models/agents/agent/agent.py +2 -1
- waldiez/models/agents/agent/update_system_message.py +0 -2
- waldiez/models/agents/doc_agent/doc_agent.py +8 -1
- waldiez/models/chat/chat.py +1 -0
- waldiez/models/chat/chat_data.py +0 -2
- waldiez/models/common/base.py +2 -0
- waldiez/models/common/dict_utils.py +169 -40
- waldiez/models/common/handoff.py +2 -0
- waldiez/models/common/method_utils.py +2 -0
- waldiez/models/flow/flow.py +6 -6
- waldiez/models/flow/info.py +5 -1
- waldiez/models/model/_llm.py +31 -14
- waldiez/models/model/model.py +4 -1
- waldiez/models/model/model_data.py +18 -5
- waldiez/models/tool/predefined/_config.py +5 -1
- waldiez/models/tool/predefined/_duckduckgo.py +4 -0
- waldiez/models/tool/predefined/_email.py +477 -0
- waldiez/models/tool/predefined/_google.py +4 -1
- waldiez/models/tool/predefined/_perplexity.py +4 -1
- waldiez/models/tool/predefined/_searxng.py +4 -1
- waldiez/models/tool/predefined/_tavily.py +4 -1
- waldiez/models/tool/predefined/_wikipedia.py +5 -2
- waldiez/models/tool/predefined/_youtube.py +4 -1
- waldiez/models/tool/predefined/protocol.py +3 -0
- waldiez/models/tool/tool.py +22 -4
- waldiez/models/waldiez.py +12 -0
- waldiez/runner.py +37 -54
- waldiez/running/__init__.py +6 -0
- waldiez/running/base_runner.py +381 -363
- waldiez/running/environment.py +1 -0
- waldiez/running/exceptions.py +9 -0
- waldiez/running/post_run.py +10 -4
- waldiez/running/pre_run.py +199 -66
- waldiez/running/protocol.py +21 -101
- waldiez/running/run_results.py +1 -1
- waldiez/running/standard_runner.py +83 -276
- waldiez/running/step_by_step/__init__.py +46 -0
- waldiez/running/step_by_step/breakpoints_mixin.py +512 -0
- waldiez/running/step_by_step/command_handler.py +151 -0
- waldiez/running/step_by_step/events_processor.py +199 -0
- waldiez/running/step_by_step/step_by_step_models.py +541 -0
- waldiez/running/step_by_step/step_by_step_runner.py +750 -0
- waldiez/running/subprocess_runner/__base__.py +279 -0
- waldiez/running/subprocess_runner/__init__.py +16 -0
- waldiez/running/subprocess_runner/_async_runner.py +362 -0
- waldiez/running/subprocess_runner/_sync_runner.py +456 -0
- waldiez/running/subprocess_runner/runner.py +570 -0
- waldiez/running/timeline_processor.py +1 -1
- waldiez/running/utils.py +492 -3
- waldiez/utils/version.py +2 -6
- waldiez/ws/__init__.py +71 -0
- waldiez/ws/__main__.py +15 -0
- waldiez/ws/_file_handler.py +199 -0
- waldiez/ws/_mock.py +74 -0
- waldiez/ws/cli.py +235 -0
- waldiez/ws/client_manager.py +851 -0
- waldiez/ws/errors.py +416 -0
- waldiez/ws/models.py +988 -0
- waldiez/ws/reloader.py +363 -0
- waldiez/ws/server.py +508 -0
- waldiez/ws/session_manager.py +393 -0
- waldiez/ws/session_stats.py +83 -0
- waldiez/ws/utils.py +410 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/METADATA +105 -96
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/RECORD +108 -83
- waldiez/running/patch_io_stream.py +0 -210
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/WHEEL +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/entry_points.txt +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {waldiez-0.5.9.dist-info → waldiez-0.6.0.dist-info}/licenses/NOTICE.md +0 -0
waldiez/ws/utils.py
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0.
|
|
2
|
+
# Copyright (c) 2024 - 2025 Waldiez and contributors.
|
|
3
|
+
# pylint: disable=import-error,line-too-long
|
|
4
|
+
# pyright: reportUnknownMemberType=false,reportUnknownVariableType=false
|
|
5
|
+
# pyright: reportUnknownArgumentType=false,reportAttributeAccessIssue=false
|
|
6
|
+
# flake8: noqa: E501
|
|
7
|
+
"""Utilities for WebSocket server management."""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import socket
|
|
13
|
+
import time
|
|
14
|
+
from contextlib import closing
|
|
15
|
+
from dataclasses import asdict, dataclass
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import websockets # type: ignore[unused-ignore, unused-import, import-not-found, import-untyped] # noqa
|
|
20
|
+
except ImportError: # pragma: no cover
|
|
21
|
+
from ._mock import websockets # type: ignore[no-redef,unused-ignore]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from .server import WaldiezWsServer
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ErrorStats:
|
|
30
|
+
"""Error stats."""
|
|
31
|
+
|
|
32
|
+
total_errors: int
|
|
33
|
+
error_counts: dict[str, int]
|
|
34
|
+
most_common_error: str | None = None
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> dict[str, Any]:
|
|
37
|
+
"""Convert to dictionary.
|
|
38
|
+
|
|
39
|
+
Returns
|
|
40
|
+
-------
|
|
41
|
+
dict[str, Any]
|
|
42
|
+
Dictionary representation of the error stats
|
|
43
|
+
"""
|
|
44
|
+
return asdict(self)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ServerHealth:
|
|
49
|
+
"""Server health status."""
|
|
50
|
+
|
|
51
|
+
status: str # "healthy", "degraded", "unhealthy"
|
|
52
|
+
uptime_seconds: float
|
|
53
|
+
active_connections: int
|
|
54
|
+
total_connections: int
|
|
55
|
+
messages_received: int
|
|
56
|
+
messages_sent: int
|
|
57
|
+
memory_usage_mb: float | None = None
|
|
58
|
+
timestamp: float = 0.0
|
|
59
|
+
error_stats: ErrorStats | None = None
|
|
60
|
+
|
|
61
|
+
def __post_init__(self) -> None:
|
|
62
|
+
"""Set timestamp after initialization."""
|
|
63
|
+
if self.timestamp == 0.0:
|
|
64
|
+
self.timestamp = time.time()
|
|
65
|
+
if not self.error_stats:
|
|
66
|
+
self.error_stats = ErrorStats(
|
|
67
|
+
total_errors=0, error_counts={}, most_common_error=None
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> dict[str, Any]:
|
|
71
|
+
"""Convert to dictionary.
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
dict[str, Any]
|
|
76
|
+
Dictionary representation of the health status
|
|
77
|
+
"""
|
|
78
|
+
my_dict = asdict(self)
|
|
79
|
+
my_dict["error_stats"] = (
|
|
80
|
+
self.error_stats.to_dict() if self.error_stats else None
|
|
81
|
+
)
|
|
82
|
+
return my_dict
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class HealthChecker:
|
|
86
|
+
"""Health checker for WebSocket server."""
|
|
87
|
+
|
|
88
|
+
def __init__(self, server: "WaldiezWsServer"):
|
|
89
|
+
"""Initialize health checker.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
server : WaldiezWsServer
|
|
94
|
+
Server instance to monitor
|
|
95
|
+
"""
|
|
96
|
+
self.server = server
|
|
97
|
+
self.check_interval = 30.0 # seconds
|
|
98
|
+
self.task: asyncio.Task[Any] | None = None
|
|
99
|
+
self.last_health: ServerHealth | None = None
|
|
100
|
+
|
|
101
|
+
def start(self) -> None:
|
|
102
|
+
"""Start health monitoring."""
|
|
103
|
+
if self.task and not self.task.done():
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
self.task = asyncio.create_task(self._monitor_loop())
|
|
107
|
+
|
|
108
|
+
def stop(self) -> None:
|
|
109
|
+
"""Stop health monitoring."""
|
|
110
|
+
if self.task and not self.task.done(): # pragma: no branch
|
|
111
|
+
self.task.cancel()
|
|
112
|
+
|
|
113
|
+
async def _monitor_loop(self) -> None:
|
|
114
|
+
"""Health monitoring loop."""
|
|
115
|
+
while True:
|
|
116
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
117
|
+
try:
|
|
118
|
+
await asyncio.sleep(self.check_interval)
|
|
119
|
+
health = await self.check_health()
|
|
120
|
+
self.last_health = health
|
|
121
|
+
|
|
122
|
+
# Log health status
|
|
123
|
+
if health.status != "healthy":
|
|
124
|
+
logger = logging.getLogger(__name__)
|
|
125
|
+
logger.warning("Server health: %s", health.status)
|
|
126
|
+
|
|
127
|
+
except asyncio.CancelledError:
|
|
128
|
+
break
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger = logging.getLogger(__name__)
|
|
131
|
+
logger.error("Health check error: %s", e)
|
|
132
|
+
|
|
133
|
+
async def check_health(self) -> ServerHealth:
|
|
134
|
+
"""Check server health.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
ServerHealth
|
|
139
|
+
Current health status
|
|
140
|
+
"""
|
|
141
|
+
stats = self.server.get_stats()
|
|
142
|
+
# error_handler = self.server.error_handler
|
|
143
|
+
|
|
144
|
+
# Determine health status
|
|
145
|
+
status = "healthy"
|
|
146
|
+
|
|
147
|
+
# Check for error rate
|
|
148
|
+
total_messages = stats["messages_received"] + stats["messages_sent"]
|
|
149
|
+
if total_messages > 0:
|
|
150
|
+
error_rate = stats["errors_total"] / total_messages
|
|
151
|
+
if error_rate > 0.1: # More than 10% errors
|
|
152
|
+
status = "unhealthy"
|
|
153
|
+
elif error_rate > 0.05: # More than 5% errors
|
|
154
|
+
status = "degraded"
|
|
155
|
+
|
|
156
|
+
# Check if server is running
|
|
157
|
+
if not stats["is_running"]:
|
|
158
|
+
status = "unhealthy"
|
|
159
|
+
|
|
160
|
+
# Get memory usage (optional)
|
|
161
|
+
memory_usage: float | None = None
|
|
162
|
+
try:
|
|
163
|
+
# pylint: disable=import-outside-toplevel
|
|
164
|
+
# noinspection PyUnusedImports
|
|
165
|
+
import psutil
|
|
166
|
+
|
|
167
|
+
process = psutil.Process()
|
|
168
|
+
memory_usage = process.memory_info().rss / 1024 / 1024 # MB
|
|
169
|
+
except ImportError:
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
return ServerHealth(
|
|
173
|
+
status=status,
|
|
174
|
+
uptime_seconds=stats["uptime_seconds"],
|
|
175
|
+
active_connections=stats["connections_active"],
|
|
176
|
+
total_connections=stats["connections_total"],
|
|
177
|
+
messages_received=stats["messages_received"],
|
|
178
|
+
messages_sent=stats["messages_sent"],
|
|
179
|
+
memory_usage_mb=memory_usage,
|
|
180
|
+
error_stats=stats["error_stats"],
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def get_last_health(self) -> ServerHealth | None:
|
|
184
|
+
"""Get last health check result.
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
Optional[ServerHealth]
|
|
189
|
+
Last health status or None if no check performed
|
|
190
|
+
"""
|
|
191
|
+
return self.last_health
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# noinspection PyBroadException
|
|
195
|
+
class ConnectionManager:
|
|
196
|
+
"""Manages WebSocket connections and provides utilities."""
|
|
197
|
+
|
|
198
|
+
def __init__(self, server: "WaldiezWsServer"):
|
|
199
|
+
"""Initialize connection manager.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
server : WaldiezWsServer
|
|
204
|
+
Server instance
|
|
205
|
+
"""
|
|
206
|
+
self.server = server
|
|
207
|
+
|
|
208
|
+
async def ping_all_clients(self) -> dict[str, bool]:
|
|
209
|
+
"""Ping all connected clients.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
dict[str, bool]
|
|
214
|
+
Map of client_id to ping success status
|
|
215
|
+
"""
|
|
216
|
+
results: dict[str, bool] = {}
|
|
217
|
+
ping_message: dict[str, Any] = {
|
|
218
|
+
"type": "pong",
|
|
219
|
+
"timestamp": time.time(),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for client_id, client in self.server.clients.items():
|
|
223
|
+
try:
|
|
224
|
+
success = await client.send_message(ping_message)
|
|
225
|
+
results[client_id] = success
|
|
226
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
227
|
+
results[client_id] = False
|
|
228
|
+
|
|
229
|
+
return results
|
|
230
|
+
|
|
231
|
+
async def disconnect_client(
|
|
232
|
+
self, client_id: str, reason: str = "Server initiated"
|
|
233
|
+
) -> bool:
|
|
234
|
+
"""Disconnect a specific client.
|
|
235
|
+
|
|
236
|
+
Parameters
|
|
237
|
+
----------
|
|
238
|
+
client_id : str
|
|
239
|
+
Client to disconnect
|
|
240
|
+
reason : str
|
|
241
|
+
Disconnection reason
|
|
242
|
+
|
|
243
|
+
Returns
|
|
244
|
+
-------
|
|
245
|
+
bool
|
|
246
|
+
True if client was disconnected successfully
|
|
247
|
+
"""
|
|
248
|
+
if client_id not in self.server.clients:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
client = self.server.clients[client_id]
|
|
252
|
+
try:
|
|
253
|
+
await client.send_message(
|
|
254
|
+
{
|
|
255
|
+
"type": "disconnect",
|
|
256
|
+
"reason": reason,
|
|
257
|
+
"timestamp": time.time(),
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
await client.websocket.close(code=1000, reason=reason)
|
|
261
|
+
return True
|
|
262
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
263
|
+
return False
|
|
264
|
+
|
|
265
|
+
def get_client_info(self, client_id: str) -> dict[str, Any] | None:
|
|
266
|
+
"""Get information about a specific client.
|
|
267
|
+
|
|
268
|
+
Parameters
|
|
269
|
+
----------
|
|
270
|
+
client_id : str
|
|
271
|
+
Client ID
|
|
272
|
+
|
|
273
|
+
Returns
|
|
274
|
+
-------
|
|
275
|
+
dict[str, Any] | None
|
|
276
|
+
Client information or None if not found
|
|
277
|
+
"""
|
|
278
|
+
if client_id not in self.server.clients:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
client = self.server.clients[client_id]
|
|
282
|
+
return {
|
|
283
|
+
"client_id": client_id,
|
|
284
|
+
"remote_address": client.remote_address,
|
|
285
|
+
"user_agent": client.user_agent,
|
|
286
|
+
"is_active": client.is_active,
|
|
287
|
+
"connection_time": client.connection_time,
|
|
288
|
+
"connection_duration": client.connection_duration,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def list_clients(self) -> dict[str, dict[str, Any]]:
|
|
292
|
+
"""List all connected clients.
|
|
293
|
+
|
|
294
|
+
Returns
|
|
295
|
+
-------
|
|
296
|
+
dict[str, dict[str, Any]]
|
|
297
|
+
Map of client_id to client information
|
|
298
|
+
"""
|
|
299
|
+
clients = {
|
|
300
|
+
client_id: self.get_client_info(client_id)
|
|
301
|
+
for client_id in self.server.clients
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
client_id: info
|
|
305
|
+
for client_id, info in clients.items()
|
|
306
|
+
if info is not None
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
async def test_server_connection(
|
|
311
|
+
host: str = "localhost", port: int = 8765, timeout: float = 5.0
|
|
312
|
+
) -> dict[str, Any]:
|
|
313
|
+
"""Test connection to WebSocket server.
|
|
314
|
+
|
|
315
|
+
Parameters
|
|
316
|
+
----------
|
|
317
|
+
host : str
|
|
318
|
+
Server host
|
|
319
|
+
port : int
|
|
320
|
+
Server port
|
|
321
|
+
timeout : float
|
|
322
|
+
Connection timeout
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
dict[str, Any]
|
|
327
|
+
Test results
|
|
328
|
+
"""
|
|
329
|
+
start_time = time.time()
|
|
330
|
+
result: dict[str, Any] = {
|
|
331
|
+
"success": False,
|
|
332
|
+
"error": None,
|
|
333
|
+
"response_time_ms": 0.0,
|
|
334
|
+
"server_response": None,
|
|
335
|
+
}
|
|
336
|
+
# pylint: disable=too-many-try-statements,broad-exception-caught
|
|
337
|
+
try:
|
|
338
|
+
uri = f"ws://{host}:{port}"
|
|
339
|
+
|
|
340
|
+
async with websockets.connect(
|
|
341
|
+
uri,
|
|
342
|
+
ping_interval=None,
|
|
343
|
+
) as websocket:
|
|
344
|
+
# Send ping message
|
|
345
|
+
ping_msg = {"action": "ping"}
|
|
346
|
+
await websocket.send(json.dumps(ping_msg))
|
|
347
|
+
|
|
348
|
+
# Wait for response
|
|
349
|
+
response = await asyncio.wait_for(websocket.recv(), timeout=timeout)
|
|
350
|
+
result["server_response"] = json.loads(response)
|
|
351
|
+
result["success"] = True
|
|
352
|
+
|
|
353
|
+
except asyncio.TimeoutError:
|
|
354
|
+
result["error"] = "Connection timeout"
|
|
355
|
+
except ConnectionRefusedError:
|
|
356
|
+
result["error"] = "Connection refused - server may not be running"
|
|
357
|
+
except Exception as e:
|
|
358
|
+
result["error"] = str(e)
|
|
359
|
+
|
|
360
|
+
result["response_time_ms"] = (time.time() - start_time) * 1000
|
|
361
|
+
return result
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# noinspection PyBroadException
|
|
365
|
+
def is_port_available(port: int) -> bool:
|
|
366
|
+
"""Check if the port is available.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
port : int
|
|
371
|
+
Port number
|
|
372
|
+
|
|
373
|
+
Returns
|
|
374
|
+
-------
|
|
375
|
+
bool
|
|
376
|
+
True if port is available
|
|
377
|
+
"""
|
|
378
|
+
# Check IPv4
|
|
379
|
+
try:
|
|
380
|
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
|
|
381
|
+
sock.bind(("", port))
|
|
382
|
+
except BaseException: # pylint: disable=broad-exception-caught
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
# Check IPv6
|
|
386
|
+
try: # pragma: no cover
|
|
387
|
+
with closing(
|
|
388
|
+
socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
|
389
|
+
) as sock:
|
|
390
|
+
# Disable dual-stack to only check IPv6
|
|
391
|
+
sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
392
|
+
sock.bind(("", port))
|
|
393
|
+
except BaseException: # pylint: disable=broad-exception-caught
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
return True
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def get_available_port() -> int:
|
|
400
|
+
"""Get an available port.
|
|
401
|
+
|
|
402
|
+
Returns
|
|
403
|
+
-------
|
|
404
|
+
int
|
|
405
|
+
An available port number
|
|
406
|
+
"""
|
|
407
|
+
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as soc:
|
|
408
|
+
soc.bind(("", 0))
|
|
409
|
+
soc.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
410
|
+
return soc.getsockname()[1]
|