ipscout 1.0.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.
ipscout/__init__.py ADDED
@@ -0,0 +1,137 @@
1
+ """ipscout - network reachability and local-network inspection, without admin rights.
2
+
3
+ Probes hosts and inspects the local network entirely in-process: no subprocess
4
+ is ever spawned, and no administrator or root privileges are required for the
5
+ default paths.
6
+
7
+ How that is possible:
8
+ On Linux and macOS ICMP goes over ``SOCK_DGRAM``/``IPPROTO_ICMP``, the
9
+ unprivileged "ping socket", where the kernel handles the privileged part.
10
+ On Windows it goes through ``iphlpapi.dll`` via ctypes, which likewise
11
+ needs no elevation - unlike raw sockets, which do.
12
+
13
+ The error contract:
14
+ Setup problems raise; network conditions do not. A down host, a timeout, or
15
+ total packet loss all return a ``ResponseObject`` with ``reached=False``.
16
+ Missing permission, an unresolvable name, or an invalid argument raise.
17
+ ``is_reachable`` is the deliberate exception - it never raises.
18
+
19
+ Examples:
20
+ >>> import ipscout
21
+ >>> result = ipscout.ping("127.0.0.1", 2, interval=0)
22
+ >>> result.reached
23
+ True
24
+ >>> ipscout.is_reachable("127.0.0.1")
25
+ True
26
+
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from .__init__conf__ import print_info
32
+ from .api import (
33
+ ais_reachable,
34
+ aping,
35
+ aping_many,
36
+ is_reachable,
37
+ ping,
38
+ ping_many,
39
+ )
40
+ from .errors import (
41
+ IPScoutError,
42
+ IPScoutPermissionError,
43
+ IPScoutResolutionError,
44
+ IPScoutUnsupportedError,
45
+ )
46
+ from .factory import icmp_available
47
+ from .interfaces import local_interfaces
48
+ from .models import (
49
+ AddressFamily,
50
+ CapabilityReport,
51
+ CommandName,
52
+ Interface,
53
+ InterfaceAddress,
54
+ LeaseInfo,
55
+ MacLookup,
56
+ MacScope,
57
+ Neighbour,
58
+ NeighbourState,
59
+ PackageInfo,
60
+ PortState,
61
+ ProbeMethod,
62
+ ReachabilityReport,
63
+ ResolveReport,
64
+ ResponseObject,
65
+ ReverseDnsReport,
66
+ RouteInfo,
67
+ ScanMethod,
68
+ SubnetInfo,
69
+ TraceHop,
70
+ )
71
+ from .mtu import path_mtu
72
+ from .neighbours import get_mac_address, lookup_mac, neighbours, normalise_mac
73
+ from .portscan import ascan_ports, parse_ports, scan_ports
74
+ from .resolve import resolve, reverse_dns
75
+ from .routes import default_gateway, query_route
76
+ from .scan import arp_scan, find_ip_by_mac, local_networks
77
+ from .subnet import subnet_info
78
+ from .traceroute import atrace_path, atraceroute, trace_path, traceroute
79
+ from .wol import wake_on_lan
80
+
81
+ __all__ = [
82
+ "AddressFamily",
83
+ "CapabilityReport",
84
+ "CommandName",
85
+ "IPScoutError",
86
+ "IPScoutPermissionError",
87
+ "IPScoutResolutionError",
88
+ "IPScoutUnsupportedError",
89
+ "Interface",
90
+ "InterfaceAddress",
91
+ "LeaseInfo",
92
+ "MacLookup",
93
+ "MacScope",
94
+ "Neighbour",
95
+ "NeighbourState",
96
+ "PackageInfo",
97
+ "PortState",
98
+ "ProbeMethod",
99
+ "ReachabilityReport",
100
+ "ResolveReport",
101
+ "ResponseObject",
102
+ "ReverseDnsReport",
103
+ "RouteInfo",
104
+ "ScanMethod",
105
+ "SubnetInfo",
106
+ "TraceHop",
107
+ "ais_reachable",
108
+ "aping",
109
+ "aping_many",
110
+ "arp_scan",
111
+ "ascan_ports",
112
+ "atrace_path",
113
+ "atraceroute",
114
+ "default_gateway",
115
+ "find_ip_by_mac",
116
+ "get_mac_address",
117
+ "icmp_available",
118
+ "is_reachable",
119
+ "local_interfaces",
120
+ "local_networks",
121
+ "lookup_mac",
122
+ "neighbours",
123
+ "normalise_mac",
124
+ "parse_ports",
125
+ "path_mtu",
126
+ "ping",
127
+ "ping_many",
128
+ "print_info",
129
+ "query_route",
130
+ "resolve",
131
+ "reverse_dns",
132
+ "scan_ports",
133
+ "subnet_info",
134
+ "trace_path",
135
+ "traceroute",
136
+ "wake_on_lan",
137
+ ]
@@ -0,0 +1,79 @@
1
+ """Static package metadata surfaced to CLI commands and documentation.
2
+
3
+ Purpose
4
+ -------
5
+ Expose the current project metadata as simple constants. These values are kept
6
+ in sync with ``pyproject.toml`` by development automation (tests, push
7
+ pipelines), so runtime code does not query packaging metadata.
8
+
9
+ Contents
10
+ --------
11
+ * Module-level constants describing the published package.
12
+ * :func:`print_info` rendering the constants for the CLI ``info`` command.
13
+
14
+ System Role
15
+ -----------
16
+ Lives in the adapters/platform layer; CLI transports import these constants to
17
+ present authoritative project information without invoking packaging APIs.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+
24
+ #: Distribution name declared in ``pyproject.toml``.
25
+ name = "ipscout"
26
+ #: Human-readable summary shown in CLI help output.
27
+ title = "Ping, traceroute and local-network inspection in pure Python - no subprocess, no admin rights"
28
+ #: Current release version pulled from ``pyproject.toml`` by automation.
29
+ version = "1.0.0"
30
+ #: Repository homepage presented to users.
31
+ homepage = "https://github.com/bitranox/ipscout"
32
+ #: Author attribution surfaced in CLI output.
33
+ author = "bitranox"
34
+ #: Contact email surfaced in CLI output.
35
+ author_email = "bitranox@gmail.com"
36
+ #: Console-script name published by the package.
37
+ shell_command = "ipscout"
38
+
39
+ #: Vendor identifier for lib_layered_config paths (macOS/Windows)
40
+ LAYEREDCONF_VENDOR: str = "bitranox"
41
+ #: Application display name for lib_layered_config paths (macOS/Windows)
42
+ LAYEREDCONF_APP: str = "ipscout"
43
+ #: Configuration slug for lib_layered_config Linux paths and environment variables
44
+ LAYEREDCONF_SLUG: str = "ipscout"
45
+
46
+
47
+ def print_info() -> None:
48
+ """Print the summarised metadata block used by the CLI ``info`` command.
49
+
50
+ Why
51
+ Provides a single, auditable rendering function so documentation and
52
+ CLI output always match the system design reference.
53
+
54
+ Side Effects
55
+ Writes to ``stdout``.
56
+
57
+ Examples
58
+ --------
59
+ >>> print_info() # doctest: +ELLIPSIS
60
+ Info for ipscout:
61
+ ...
62
+ """
63
+
64
+ fields = [
65
+ ("name", name),
66
+ ("title", title),
67
+ ("version", version),
68
+ ("homepage", homepage),
69
+ ("author", author),
70
+ ("author_email", author_email),
71
+ ("shell_command", shell_command),
72
+ ]
73
+ pad = max(len(label) for label, _ in fields)
74
+ lines = [f"Info for {name}:", ""]
75
+ lines.extend(f" {label.ljust(pad)} = {value}" for label, value in fields)
76
+ sys.stdout.write("\n".join(lines) + "\n")
77
+
78
+
79
+ __all__ = ["print_info"]
ipscout/__main__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Module entry point for python -m execution.
2
+
3
+ Provide the ``python -m ipscout`` path mandated by the
4
+ project's packaging guidelines. This delegates to the CLI main function
5
+ ensuring consistent behavior between module execution and console scripts.
6
+
7
+ Note:
8
+ Lives in the adapters layer. It bridges CPython's module execution entry
9
+ point to the shared CLI helper defined in cli.py.
10
+
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .cli import main
16
+
17
+ if __name__ == "__main__":
18
+ raise SystemExit(main())
ipscout/api.py ADDED
@@ -0,0 +1,425 @@
1
+ """The public callables. Everything a user of this library normally touches.
2
+
3
+ This module owns the error contract, because it is the only layer that knows
4
+ both what the caller asked for and what the machine could actually do.
5
+
6
+ The contract, in one sentence: **setup problems raise, network conditions do
7
+ not.** A host that is down, a packet that times out, a link losing everything -
8
+ all come back as a ``ResponseObject`` with ``reached=False``. Missing ICMP
9
+ permission, an unresolvable name, or a nonsensical argument raise, because no
10
+ amount of retrying fixes them and reporting them as "unreachable" sends people
11
+ hunting for network faults that do not exist.
12
+
13
+ Contents:
14
+ ping / aping: Probe one target.
15
+ ping_many / aping_many: Probe many, with bounded concurrency.
16
+ is_reachable / ais_reachable: The deliberately total convenience shortcut.
17
+
18
+ Note:
19
+ ``is_reachable`` is the one exception to the contract above, on purpose:
20
+ it never raises and it always falls back to TCP. That is what makes it a
21
+ shortcut. It is documented on its own docstring so nobody mistakes it for
22
+ ``ping(...).reached``.
23
+
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import asyncio
29
+ from typing import TYPE_CHECKING
30
+
31
+ from .errors import IPScoutError
32
+ from .factory import icmp_available, make_async_transport, make_transport
33
+ from .models import AddressFamily, ProbeMethod, ResponseObject
34
+ from .pool import gather_bounded
35
+ from .resolve import resolve_one
36
+ from .service import PingRequest, arun_ping, run_ping
37
+
38
+ if TYPE_CHECKING:
39
+ from collections.abc import Iterable
40
+
41
+ __all__ = [
42
+ "ais_reachable",
43
+ "aping",
44
+ "aping_many",
45
+ "is_reachable",
46
+ "ping",
47
+ "ping_many",
48
+ ]
49
+
50
+ #: Default concurrency for a sweep. High enough to be useful, low enough not to
51
+ #: exhaust file descriptors on a default ulimit.
52
+ DEFAULT_CONCURRENCY = 64
53
+
54
+
55
+ def _prepare( # noqa: PLR0913 - public API: every knob is keyword-only and independently useful
56
+ target: str,
57
+ times: int,
58
+ timeout: float,
59
+ interval: float,
60
+ family: AddressFamily | None,
61
+ *,
62
+ use_tcp: bool,
63
+ ) -> PingRequest:
64
+ """Resolve the target and validate the parameters into a request."""
65
+
66
+ address, resolved_family = resolve_one(target, family=family)
67
+ return PingRequest(
68
+ target=target,
69
+ address=address,
70
+ family=resolved_family,
71
+ times=times,
72
+ timeout=timeout,
73
+ interval=interval,
74
+ method=ProbeMethod.TCP if use_tcp else ProbeMethod.ICMP,
75
+ )
76
+
77
+
78
+ def _suppressed(target: str, times: int, exc: Exception) -> ResponseObject:
79
+ """Return the unreached result used when ``raise_on_error=False``."""
80
+
81
+ return ResponseObject(target=target, number_of_pings=times, error=str(exc))
82
+
83
+
84
+ def ping( # noqa: PLR0913 - public API: every knob is keyword-only and independently useful
85
+ target: str,
86
+ times: int = 4,
87
+ *,
88
+ timeout: float = 2.0,
89
+ interval: float = 0.2,
90
+ family: AddressFamily | None = None,
91
+ payload_size: int = 56,
92
+ allow_tcp_fallback: bool = False,
93
+ tcp_port: int = 443,
94
+ raise_on_error: bool = True,
95
+ ) -> ResponseObject:
96
+ """Ping a target and report what came back.
97
+
98
+ Args:
99
+ target: Hostname or IP address literal.
100
+ times: How many echoes to send.
101
+ timeout: Seconds to wait for each reply.
102
+ interval: Seconds between the start of consecutive echoes.
103
+ family: Force IPv4 or IPv6. ``None`` takes the resolver's preference.
104
+ payload_size: Bytes of ICMP payload per echo.
105
+ allow_tcp_fallback: Probe over TCP when ICMP is unavailable. Off by
106
+ default: a TCP result is not an ICMP result, and substituting one
107
+ silently would misreport a filtered port as a dead host.
108
+ tcp_port: Port used only when the TCP fallback is engaged.
109
+ raise_on_error: Keep the strict contract. Set False to report every
110
+ failure as ``reached=False`` with the reason on ``.error``
111
+ instead.
112
+
113
+ Returns:
114
+ The result. Unreachable targets, timeouts and total loss are all
115
+ reported here rather than raised.
116
+
117
+ Raises:
118
+ IPScoutResolutionError: The target does not resolve.
119
+ IPScoutPermissionError: ICMP is unavailable and no fallback was allowed.
120
+ IPScoutUnsupportedError: No ICMP backend exists for this platform.
121
+ ValueError: ``times``, ``timeout`` or ``interval`` is out of range.
122
+
123
+ Examples:
124
+ >>> result = ping("127.0.0.1", 2, interval=0)
125
+ >>> result.reached, result.packets_received
126
+ (True, 2)
127
+ >>> result.ip
128
+ '127.0.0.1'
129
+
130
+ A name that does not resolve raises rather than reading as "down":
131
+
132
+ >>> ping("nothing.invalid")
133
+ Traceback (most recent call last):
134
+ ...
135
+ ipscout.errors.IPScoutResolutionError: cannot resolve 'nothing.invalid': ...
136
+
137
+ Unless the caller asks for failures to be reported rather than raised:
138
+
139
+ >>> muted = ping("nothing.invalid", raise_on_error=False)
140
+ >>> muted.reached, muted.error is None
141
+ (False, False)
142
+
143
+ """
144
+
145
+ try:
146
+ use_tcp = allow_tcp_fallback and not icmp_available(family or AddressFamily.IPV4)
147
+ request = _prepare(target, times, timeout, interval, family, use_tcp=use_tcp)
148
+ with make_transport(
149
+ request.address,
150
+ request.family,
151
+ payload_size=payload_size,
152
+ use_tcp=use_tcp,
153
+ tcp_port=tcp_port,
154
+ ) as transport:
155
+ return run_ping(request, transport)
156
+ except (IPScoutError, ValueError) as exc:
157
+ if raise_on_error:
158
+ raise
159
+ return _suppressed(target, times, exc)
160
+
161
+
162
+ async def aping( # noqa: PLR0913 - public API: every knob is keyword-only and independently useful
163
+ target: str,
164
+ times: int = 4,
165
+ *,
166
+ timeout: float = 2.0,
167
+ interval: float = 0.2,
168
+ family: AddressFamily | None = None,
169
+ payload_size: int = 56,
170
+ allow_tcp_fallback: bool = False,
171
+ tcp_port: int = 443,
172
+ raise_on_error: bool = True,
173
+ ) -> ResponseObject:
174
+ """Ping a target without blocking the event loop.
175
+
176
+ Takes the same arguments as :func:`ping` and honours the same contract.
177
+
178
+ Returns:
179
+ The result.
180
+
181
+ Note:
182
+ Genuinely asynchronous on Linux and macOS, where the ICMP socket is
183
+ registered with the event loop. On Windows the underlying
184
+ ``IcmpSendEcho`` is a blocking C call with no asyncio integration, so
185
+ it runs in a bounded thread pool instead. The behaviour is identical;
186
+ the scaling characteristics are not.
187
+
188
+ Examples:
189
+ >>> import asyncio
190
+ >>> result = asyncio.run(aping("127.0.0.1", 2, interval=0))
191
+ >>> result.reached
192
+ True
193
+
194
+ """
195
+
196
+ try:
197
+ use_tcp = allow_tcp_fallback and not icmp_available(family or AddressFamily.IPV4)
198
+ request = _prepare(target, times, timeout, interval, family, use_tcp=use_tcp)
199
+ transport = make_async_transport(
200
+ request.address,
201
+ request.family,
202
+ payload_size=payload_size,
203
+ use_tcp=use_tcp,
204
+ tcp_port=tcp_port,
205
+ )
206
+ async with transport:
207
+ return await arun_ping(request, transport)
208
+ except (IPScoutError, ValueError) as exc:
209
+ if raise_on_error:
210
+ raise
211
+ return _suppressed(target, times, exc)
212
+
213
+
214
+ async def aping_many( # noqa: PLR0913 - public API: every knob is keyword-only and independently useful
215
+ targets: Iterable[str],
216
+ *,
217
+ concurrency: int = DEFAULT_CONCURRENCY,
218
+ times: int = 4,
219
+ timeout: float = 2.0,
220
+ interval: float = 0.2,
221
+ family: AddressFamily | None = None,
222
+ payload_size: int = 56,
223
+ allow_tcp_fallback: bool = False,
224
+ tcp_port: int = 443,
225
+ raise_on_error: bool = False,
226
+ ) -> dict[str, ResponseObject]:
227
+ """Ping many targets concurrently.
228
+
229
+ Args:
230
+ targets: The targets. Duplicates are collapsed, since the result is
231
+ keyed by target and a repeat could only overwrite itself.
232
+ concurrency: Maximum probes in flight at once.
233
+ times: Echoes per target.
234
+ timeout: Seconds to wait for each reply.
235
+ interval: Seconds between one target's consecutive echoes.
236
+ family: Force IPv4 or IPv6.
237
+ payload_size: Bytes of ICMP payload per echo.
238
+ allow_tcp_fallback: Allow the TCP probe when ICMP is unavailable.
239
+ tcp_port: Port used only when the TCP fallback is engaged.
240
+ raise_on_error: Defaults to **False** here, unlike :func:`ping`. In a
241
+ sweep one bad name among two hundred should not destroy the other
242
+ 199 results; the failure is reported on that target's own
243
+ ``.error`` instead. Set True to make any single failure raise.
244
+
245
+ Returns:
246
+ A mapping of target to result, in the order the targets were given.
247
+
248
+ Examples:
249
+ >>> import asyncio
250
+ >>> results = asyncio.run(aping_many(["127.0.0.1", "::1"], times=1))
251
+ >>> sorted(results)
252
+ ['127.0.0.1', '::1']
253
+ >>> all(r.reached for r in results.values())
254
+ True
255
+
256
+ """
257
+
258
+ unique = list(dict.fromkeys(targets))
259
+
260
+ async def one(target: str) -> tuple[str, ResponseObject]:
261
+ # Returns the target with its result: the pool completes out of order,
262
+ # so pairing by position afterwards would attribute results to the
263
+ # wrong hosts.
264
+ result = await aping(
265
+ target,
266
+ times,
267
+ timeout=timeout,
268
+ interval=interval,
269
+ family=family,
270
+ payload_size=payload_size,
271
+ allow_tcp_fallback=allow_tcp_fallback,
272
+ tcp_port=tcp_port,
273
+ raise_on_error=raise_on_error,
274
+ )
275
+ return target, result
276
+
277
+ # A worker pool rather than a semaphore inside a gather over everything:
278
+ # the semaphore bounds probes in flight while still creating one Task per
279
+ # target up front, so a sweep of a large subnet holds them all at once.
280
+ completed = await gather_bounded(unique, one, concurrency)
281
+ results = dict(completed)
282
+ if len(results) != len(unique): # pragma: no cover - a lost result is a bug, not a condition
283
+ msg = f"sweep returned {len(results)} results for {len(unique)} targets"
284
+ raise RuntimeError(msg)
285
+ return results
286
+
287
+
288
+ def ping_many( # noqa: PLR0913 - public API: every knob is keyword-only and independently useful
289
+ targets: Iterable[str],
290
+ *,
291
+ concurrency: int = DEFAULT_CONCURRENCY,
292
+ times: int = 4,
293
+ timeout: float = 2.0,
294
+ interval: float = 0.2,
295
+ family: AddressFamily | None = None,
296
+ payload_size: int = 56,
297
+ allow_tcp_fallback: bool = False,
298
+ tcp_port: int = 443,
299
+ raise_on_error: bool = False,
300
+ ) -> dict[str, ResponseObject]:
301
+ """Ping many targets concurrently from synchronous code.
302
+
303
+ Takes the same arguments as :func:`aping_many`.
304
+
305
+ Returns:
306
+ A mapping of target to result, in the order the targets were given.
307
+
308
+ Raises:
309
+ RuntimeError: Called from inside a running event loop, where it would
310
+ deadlock. Await :func:`aping_many` there instead. Failing loudly
311
+ beats hanging.
312
+
313
+ Examples:
314
+ >>> results = ping_many(["127.0.0.1", "::1"], times=1)
315
+ >>> sorted(results)
316
+ ['127.0.0.1', '::1']
317
+
318
+ """
319
+
320
+ try:
321
+ asyncio.get_running_loop()
322
+ except RuntimeError:
323
+ pass
324
+ else:
325
+ msg = "ping_many() cannot run inside an event loop; await aping_many() instead"
326
+ raise RuntimeError(msg)
327
+
328
+ return asyncio.run(
329
+ aping_many(
330
+ targets,
331
+ concurrency=concurrency,
332
+ times=times,
333
+ timeout=timeout,
334
+ interval=interval,
335
+ family=family,
336
+ payload_size=payload_size,
337
+ allow_tcp_fallback=allow_tcp_fallback,
338
+ tcp_port=tcp_port,
339
+ raise_on_error=raise_on_error,
340
+ )
341
+ )
342
+
343
+
344
+ def is_reachable(
345
+ target: str,
346
+ *,
347
+ timeout: float = 2.0,
348
+ tcp_port: int = 443,
349
+ family: AddressFamily | None = None,
350
+ ) -> bool:
351
+ """Return whether a target answers, by any means available.
352
+
353
+ The deliberate exception to this module's error contract. It **never
354
+ raises** and it **always** tries TCP when ICMP does not answer or is
355
+ unavailable. That is the entire point of the shortcut.
356
+
357
+ Args:
358
+ target: Hostname or IP address literal.
359
+ timeout: Seconds to allow for each attempt.
360
+ tcp_port: Port for the TCP attempt.
361
+ family: Force IPv4 or IPv6.
362
+
363
+ Returns:
364
+ True if anything answered.
365
+
366
+ Note:
367
+ This is not ``ping(target).reached``. That reports whether *ICMP*
368
+ succeeded and raises on a bad name; this reports whether the host is
369
+ alive by any route and answers False for a bad name. Use ``ping`` when
370
+ the distinction matters, and this when it does not.
371
+
372
+ Examples:
373
+ >>> is_reachable("127.0.0.1")
374
+ True
375
+ >>> is_reachable("nothing.invalid")
376
+ False
377
+
378
+ """
379
+
380
+ if ping(target, 1, timeout=timeout, interval=0, family=family, raise_on_error=False).reached:
381
+ return True
382
+
383
+ # Fall through to TCP unconditionally. Going via ping(allow_tcp_fallback=True)
384
+ # would not do it: that only substitutes TCP when ICMP is *unavailable*, and
385
+ # the common case here is ICMP being available but the target not answering it.
386
+ try:
387
+ address, resolved = resolve_one(target, family=family)
388
+ with make_transport(address, resolved, use_tcp=True, tcp_port=tcp_port) as transport:
389
+ return transport.probe(sequence=1, timeout=timeout).answered
390
+ except (IPScoutError, ValueError, OSError):
391
+ return False
392
+
393
+
394
+ async def ais_reachable(
395
+ target: str,
396
+ *,
397
+ timeout: float = 2.0,
398
+ tcp_port: int = 443,
399
+ family: AddressFamily | None = None,
400
+ ) -> bool:
401
+ """Return whether a target answers, without blocking the event loop.
402
+
403
+ Same total, never-raising contract as :func:`is_reachable`.
404
+
405
+ Returns:
406
+ True if anything answered.
407
+
408
+ Examples:
409
+ >>> import asyncio
410
+ >>> asyncio.run(ais_reachable("127.0.0.1"))
411
+ True
412
+
413
+ """
414
+
415
+ icmp = await aping(target, 1, timeout=timeout, interval=0, family=family, raise_on_error=False)
416
+ if icmp.reached:
417
+ return True
418
+
419
+ try:
420
+ address, resolved = resolve_one(target, family=family)
421
+ transport = make_async_transport(address, resolved, use_tcp=True, tcp_port=tcp_port)
422
+ async with transport:
423
+ return (await transport.probe(sequence=1, timeout=timeout)).answered
424
+ except (IPScoutError, ValueError, OSError):
425
+ return False