aiodiscover 2.2.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.
@@ -0,0 +1,13 @@
1
+ """Top-level package for Async Host discovery."""
2
+
3
+ __author__ = "J. Nick Koston"
4
+ __email__ = "nick@koston.org"
5
+ # Do not edit this string manually, always use bumpversion
6
+ # Details in CONTRIBUTING.md
7
+ __version__ = "2.2.0"
8
+
9
+ from .discovery import DiscoverHosts # noqa: F401
10
+
11
+
12
+ def get_module_version() -> str:
13
+ return __version__
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections.abc import Iterable
6
+ from contextlib import suppress
7
+ from functools import lru_cache, partial
8
+ from ipaddress import IPv4Address
9
+ from itertools import islice
10
+ from typing import TYPE_CHECKING, Any, cast
11
+
12
+ from aiodns import DNSResolver
13
+
14
+ from .network import SystemNetworkData
15
+
16
+ if TYPE_CHECKING:
17
+ from pyroute2.iproute import IPRoute
18
+
19
+ HOSTNAME = "hostname"
20
+ MAC_ADDRESS = "macaddress"
21
+ IP_ADDRESS = "ip"
22
+ MAX_ADDRESSES = 2048
23
+ QUERY_BUCKET_SIZE = 64
24
+
25
+ DNS_RESPONSE_TIMEOUT = 2
26
+
27
+
28
+ _LOGGER = logging.getLogger(__name__)
29
+
30
+
31
+ @lru_cache(maxsize=MAX_ADDRESSES)
32
+ def decode_idna(name: str) -> str:
33
+ """Decode an idna name."""
34
+ try:
35
+ return name.encode().decode("idna")
36
+ except UnicodeError:
37
+ return name
38
+
39
+
40
+ def dns_message_short_hostname(dns_message: Any | None) -> str | None:
41
+ """Get the short hostname from a dns message."""
42
+ if dns_message is None:
43
+ return None
44
+ name: str = dns_message.name
45
+ if name.startswith("xn--"):
46
+ name = decode_idna(name)
47
+ return name.partition(".")[0]
48
+
49
+
50
+ async def async_query_for_ptrs(
51
+ nameserver: str, ips_to_lookup: list[IPv4Address]
52
+ ) -> list[Any | None]:
53
+ """Fetch PTR records for a list of ips."""
54
+ resolver = DNSResolver(nameservers=[nameserver], timeout=DNS_RESPONSE_TIMEOUT)
55
+ results: list[Any | None] = []
56
+ for ip_chunk in chunked(ips_to_lookup, QUERY_BUCKET_SIZE):
57
+ if TYPE_CHECKING:
58
+ ip_chunk = cast("list[IPv4Address]", ip_chunk)
59
+ futures = [resolver.query(ip.reverse_pointer, "PTR") for ip in ip_chunk]
60
+ await asyncio.wait(futures)
61
+ results.extend(
62
+ None if future.exception() else future.result() for future in futures
63
+ )
64
+ resolver.cancel()
65
+ return results
66
+
67
+
68
+ def take(take_num: int, iterable: Iterable[Any]) -> list[Any]:
69
+ """
70
+ Return first n items of the iterable as a list.
71
+
72
+ From itertools recipes
73
+ """
74
+ return list(islice(iterable, take_num))
75
+
76
+
77
+ def chunked(iterable: Iterable[Any], chunked_num: int) -> Iterable[Any]:
78
+ """
79
+ Break *iterable* into lists of length *n*.
80
+
81
+ From more-itertools
82
+ """
83
+ return iter(partial(take, chunked_num, iter(iterable)), [])
84
+
85
+
86
+ class DiscoverHosts:
87
+ """Discover hosts on the network by ARP and PTR lookup."""
88
+
89
+ def __init__(self) -> None:
90
+ """Init the discovery hosts."""
91
+ self._sys_network_data: SystemNetworkData | None = None
92
+
93
+ def _setup_sys_network_data(self) -> None:
94
+ ip_route: IPRoute | None = None
95
+ with suppress(Exception):
96
+ from pyroute2.iproute import (
97
+ IPRoute,
98
+ )
99
+
100
+ ip_route = IPRoute()
101
+ sys_network_data = SystemNetworkData(ip_route)
102
+ sys_network_data.setup()
103
+ self._sys_network_data = sys_network_data
104
+
105
+ async def async_discover(self) -> list[dict[str, str]]:
106
+ """Discover hosts on the network by ARP and PTR lookup."""
107
+ if not self._sys_network_data:
108
+ await asyncio.get_running_loop().run_in_executor(
109
+ None, self._setup_sys_network_data
110
+ )
111
+ if TYPE_CHECKING:
112
+ assert self._sys_network_data is not None
113
+ sys_network_data = self._sys_network_data
114
+ network = sys_network_data.network
115
+ if TYPE_CHECKING:
116
+ assert network is not None
117
+ if network.num_addresses > MAX_ADDRESSES:
118
+ _LOGGER.debug(
119
+ "The network %s exceeds the maximum number of addresses, %s; No scanning performed",
120
+ network,
121
+ MAX_ADDRESSES,
122
+ )
123
+ return []
124
+ hostnames = await self.async_get_hostnames(sys_network_data)
125
+ neighbours = await sys_network_data.async_get_neighbors(hostnames.keys())
126
+ return [
127
+ {
128
+ HOSTNAME: hostname,
129
+ MAC_ADDRESS: neighbours[ip],
130
+ IP_ADDRESS: ip,
131
+ }
132
+ for ip, hostname in hostnames.items()
133
+ if ip in neighbours
134
+ ]
135
+
136
+ async def _async_get_nameservers(
137
+ self, sys_network_data: SystemNetworkData
138
+ ) -> list[str]:
139
+ """Get nameservers to query."""
140
+ all_nameservers = list(sys_network_data.nameservers)
141
+ router_ip = sys_network_data.router_ip
142
+ assert router_ip is not None
143
+ if router_ip not in all_nameservers:
144
+ neighbours = await sys_network_data.async_get_neighbors([router_ip])
145
+ if router_ip in neighbours:
146
+ all_nameservers.insert(0, router_ip)
147
+ return all_nameservers
148
+
149
+ async def async_get_hostnames(
150
+ self, sys_network_data: SystemNetworkData
151
+ ) -> dict[str, str]:
152
+ """Lookup PTR records for all addresses in the network."""
153
+ all_nameservers = await self._async_get_nameservers(sys_network_data)
154
+ assert sys_network_data.network is not None
155
+ ips = list(sys_network_data.network.hosts())
156
+ hostnames: dict[str, str] = {}
157
+ for nameserver in all_nameservers:
158
+ ips_to_lookup = [ip for ip in ips if str(ip) not in hostnames]
159
+ results = await async_query_for_ptrs(nameserver, ips_to_lookup)
160
+ for idx, ip in enumerate(ips_to_lookup):
161
+ short_host = dns_message_short_hostname(results[idx])
162
+ if short_host is None:
163
+ continue
164
+ hostnames[str(ip)] = short_host
165
+ if hostnames:
166
+ # As soon as we have a responsive nameserver, there
167
+ # is no need to query additional fallbacks
168
+ break
169
+ return hostnames
aiodiscover/network.py ADDED
@@ -0,0 +1,262 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import re
5
+ import socket
6
+ import sys
7
+ from collections.abc import Iterable
8
+ from contextlib import suppress
9
+ from ipaddress import IPv4Network, ip_network
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ import ifaddr # type: ignore
13
+ from cached_ipaddress import cached_ip_addresses
14
+
15
+ from .util import asyncio_timeout
16
+
17
+ if TYPE_CHECKING:
18
+ from pyroute2.iproute import IPRoute
19
+ # Some MAC addresses will drop the leading zero so
20
+ # our mac validation must allow a single char
21
+ VALID_MAC_ADDRESS = re.compile("^([0-9A-Fa-f]{1,2}[:-]){5}([0-9A-Fa-f]{1,2})$")
22
+
23
+ ARP_CACHE_POPULATE_TIME = 10
24
+ ARP_TIMEOUT = 10
25
+
26
+ DEFAULT_NETWORK_PREFIX = 24
27
+
28
+
29
+ PRIVATE_AND_LOCAL_NETWORKS = (
30
+ ip_network("127.0.0.0/8"),
31
+ ip_network("10.0.0.0/8"),
32
+ ip_network("172.16.0.0/12"),
33
+ ip_network("192.168.0.0/16"),
34
+ )
35
+
36
+ DEFAULT_TARGET = "10.255.255.255"
37
+ MDNS_TARGET_IP = "224.0.0.251"
38
+ PUBLIC_TARGET_IP = "8.8.8.8"
39
+ LOOPBACK_TARGET_IP = "127.0.0.1"
40
+
41
+ IGNORE_MACS = {"00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff"}
42
+
43
+
44
+ def load_resolv_conf() -> list[str]:
45
+ """Load the resolv.conf."""
46
+ with open("/etc/resolv.conf") as file:
47
+ lines = tuple(file)
48
+ nameservers = set()
49
+ for line in lines:
50
+ line = line.strip()
51
+ if not len(line):
52
+ continue
53
+ if line[0] in ("#", ";"):
54
+ continue
55
+ key, value = line.split(None, 1)
56
+ if key == "nameserver":
57
+ if ip_addr := cached_ip_addresses(value):
58
+ nameservers.add(ip_addr)
59
+ return list(nameservers)
60
+
61
+
62
+ def get_local_ip(target: str = DEFAULT_TARGET) -> str | None:
63
+ """Find the local ip address."""
64
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
65
+ s.setblocking(False)
66
+ try:
67
+ s.connect((target, 1))
68
+ return s.getsockname()[0]
69
+ except Exception:
70
+ return None
71
+ finally:
72
+ s.close()
73
+
74
+
75
+ def get_network(local_ip: str, adapters: Any) -> IPv4Network:
76
+ """Search adapters for the network and broadcast ip."""
77
+ network_prefix = (
78
+ get_ip_prefix_from_adapters(local_ip, adapters) or DEFAULT_NETWORK_PREFIX
79
+ )
80
+ network = ip_network(f"{local_ip}/{network_prefix}", False)
81
+ if TYPE_CHECKING:
82
+ assert isinstance(network, IPv4Network)
83
+ return network
84
+
85
+
86
+ def get_ip_prefix_from_adapters(local_ip: str, adapters: Any) -> int | None:
87
+ """Find the network prefix for an adapter."""
88
+ for adapter in adapters:
89
+ for ip in adapter.ips:
90
+ if local_ip == ip.ip:
91
+ return ip.network_prefix
92
+ return None
93
+
94
+
95
+ def get_attrs_key(data: Any, key: Any) -> Any:
96
+ """Lookup an attrs key in pyroute2 data."""
97
+ for attr_key, attr_value in data["attrs"]:
98
+ if attr_key == key:
99
+ return attr_value
100
+
101
+
102
+ def get_router_ip(ipr: IPRoute) -> Any:
103
+ """Obtain the router ip from the default route."""
104
+ return get_attrs_key(ipr.get_default_routes()[0], "RTA_GATEWAY")
105
+
106
+
107
+ def _fill_neighbor(neighbours: dict[str, str], ip: str, mac: str) -> None:
108
+ """Add a neighbor if it is valid."""
109
+ if not (ip_addr := cached_ip_addresses(ip)):
110
+ return
111
+ if (
112
+ ip_addr.is_loopback
113
+ or ip_addr.is_link_local
114
+ or ip_addr.is_multicast
115
+ or ip_addr.is_unspecified
116
+ ):
117
+ return
118
+ if not VALID_MAC_ADDRESS.match(mac):
119
+ return
120
+ mac = ":".join([i.zfill(2) for i in mac.split(":")])
121
+ if mac in IGNORE_MACS:
122
+ return
123
+ neighbours[ip] = mac
124
+
125
+
126
+ def async_populate_arp(ip_addresses: Iterable[str]) -> socket.socket:
127
+ """Send an empty packet to a host to populate the arp cache."""
128
+ sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
129
+ sock.setblocking(False)
130
+ for ip_addr in ip_addresses:
131
+ try:
132
+ sock.sendto(b"", (ip_addr, 80))
133
+ except Exception:
134
+ pass
135
+ return sock
136
+
137
+
138
+ class SystemNetworkData:
139
+ """Gather system network data."""
140
+
141
+ def __init__(self, ip_route: IPRoute | None, local_ip: str | None = None) -> None:
142
+ """Init system network data."""
143
+ self.ip_route = ip_route
144
+ self.local_ip = local_ip
145
+ self.broadcast_ip: str | None = None
146
+ self.router_ip: str | None = None
147
+ self.network: IPv4Network | None = None
148
+ self.adapters: Any = None
149
+ self.nameservers: list[str] = []
150
+
151
+ def setup(self) -> None:
152
+ """Obtain the local network data."""
153
+ try:
154
+ resolvers = load_resolv_conf()
155
+ except FileNotFoundError:
156
+ if sys.platform != "win32":
157
+ raise
158
+ else:
159
+ self.nameservers = [
160
+ str(ip_addr)
161
+ for ip_addr in resolvers
162
+ if any(ip_addr in network for network in PRIVATE_AND_LOCAL_NETWORKS)
163
+ ]
164
+ self.adapters = ifaddr.get_adapters()
165
+ if not self.local_ip:
166
+ self.local_ip = (
167
+ get_local_ip(DEFAULT_TARGET)
168
+ or get_local_ip(MDNS_TARGET_IP)
169
+ or get_local_ip(PUBLIC_TARGET_IP)
170
+ or get_local_ip(LOOPBACK_TARGET_IP)
171
+ )
172
+ assert self.local_ip is not None
173
+ self.network = get_network(self.local_ip, self.adapters)
174
+ if self.ip_route:
175
+ try:
176
+ self.router_ip = get_router_ip(self.ip_route)
177
+ except Exception:
178
+ pass
179
+ if not self.router_ip:
180
+ # On MacOS netifaces is the only reliable way to get the default gateway
181
+ with suppress(Exception):
182
+ import netifaces # type: ignore # pylint: disable=import-outside-toplevel
183
+
184
+ self.router_ip = netifaces.gateways()["default"][netifaces.AF_INET][0]
185
+ if not self.router_ip:
186
+ network_address = str(self.network.network_address)
187
+ self.router_ip = f"{network_address[:-1]}1"
188
+
189
+ async def async_get_neighbors(self, ips: Iterable[str]) -> dict[str, str]:
190
+ """Get neighbors with best available method."""
191
+ neighbors = await self._async_get_neighbors()
192
+ ips_missing_arp = [ip for ip in ips if ip not in neighbors]
193
+ if not ips_missing_arp:
194
+ return neighbors
195
+ sock = async_populate_arp(ips_missing_arp)
196
+ await asyncio.sleep(ARP_CACHE_POPULATE_TIME)
197
+ sock.close()
198
+ neighbors.update(await self._async_get_neighbors())
199
+ return neighbors
200
+
201
+ async def _async_get_neighbors(self) -> dict[str, str]:
202
+ """Get neighbors from the arp table."""
203
+ if self.ip_route:
204
+ return await self._async_get_neighbors_ip_route()
205
+ return await self._async_get_neighbors_arp()
206
+
207
+ async def _async_get_neighbors_arp(self) -> dict[str, str]:
208
+ """Get neighbors with arp command."""
209
+ neighbours: dict[str, str] = {}
210
+ arp = await asyncio.create_subprocess_exec(
211
+ "arp",
212
+ "-a",
213
+ "-n",
214
+ stdin=None,
215
+ stdout=asyncio.subprocess.PIPE,
216
+ stderr=asyncio.subprocess.PIPE,
217
+ close_fds=False,
218
+ )
219
+ try:
220
+ async with asyncio_timeout(ARP_TIMEOUT):
221
+ out_data, _ = await arp.communicate()
222
+ except asyncio.TimeoutError:
223
+ if arp:
224
+ with suppress(TypeError):
225
+ await arp.kill() # type: ignore
226
+ del arp
227
+ return neighbours
228
+ except AttributeError:
229
+ return neighbours
230
+
231
+ for line in out_data.decode().splitlines():
232
+ chomped = line.strip()
233
+ data = chomped.split()
234
+ if len(data) < 4:
235
+ continue
236
+ ip = data[1].strip("()")
237
+ mac = data[3]
238
+ _fill_neighbor(neighbours, ip, mac)
239
+
240
+ return neighbours
241
+
242
+ async def _async_get_neighbors_ip_route(self) -> dict[str, str]:
243
+ """Get neighbors with pyroute2."""
244
+ neighbours: dict[str, str] = {}
245
+ loop = asyncio.get_running_loop()
246
+ # This shouldn't ever block but it does
247
+ # interact with netlink so its safer to run
248
+ # in the executor
249
+ if TYPE_CHECKING:
250
+ assert self.ip_route is not None
251
+ for neighbour in await loop.run_in_executor(None, self.ip_route.get_neighbours):
252
+ ip = None
253
+ mac = None
254
+ for key, value in neighbour["attrs"]:
255
+ if key == "NDA_DST":
256
+ ip = value
257
+ elif key == "NDA_LLADDR":
258
+ mac = value
259
+ if ip and mac:
260
+ _fill_neighbor(neighbours, ip, mac)
261
+
262
+ return neighbours
@@ -0,0 +1 @@
1
+ """Unit test package for aiodiscover."""
@@ -0,0 +1 @@
1
+ #!/usr/bin/env python
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env python
2
+ import asyncio
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from ipaddress import IPv4Address, IPv4Network
6
+ from typing import Any
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from aiodiscover import discovery
12
+
13
+ if sys.platform == "win32":
14
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
15
+
16
+
17
+ @pytest.mark.asyncio
18
+ async def test_async_discover_hosts() -> None:
19
+ """Verify discover hosts does not throw."""
20
+ discover_hosts = discovery.DiscoverHosts()
21
+ with patch.object(discovery, "MAX_ADDRESSES", 16):
22
+ hosts = await discover_hosts.async_discover()
23
+ assert isinstance(hosts, list)
24
+
25
+
26
+ @pytest.mark.asyncio
27
+ async def test_async_discover_hosts_with_dns_mock() -> None:
28
+ """Verify discover hosts does not throw."""
29
+ discover_hosts = discovery.DiscoverHosts()
30
+ with (
31
+ patch.object(discovery, "MAX_ADDRESSES", 2),
32
+ patch(
33
+ "aiodiscover.discovery.dns_message_short_hostname", return_value="router"
34
+ ),
35
+ ):
36
+ hosts = await discover_hosts.async_discover()
37
+ assert isinstance(hosts, list)
38
+
39
+
40
+ @pytest.mark.asyncio
41
+ async def test_async_discover_hosts_with_dns_mock_neighbor_mock() -> None:
42
+ """Verify discover hosts does not throw."""
43
+ discover_hosts = discovery.DiscoverHosts()
44
+
45
+ async def _async_get_hostnames(sys_network_data: Any) -> dict[str, str]:
46
+ return {"1.2.3.4": "router", "4.5.5.6": "any"}
47
+
48
+ discover_hosts.async_get_hostnames = _async_get_hostnames # type: ignore
49
+ with (
50
+ patch(
51
+ "aiodiscover.network.SystemNetworkData.async_get_neighbors",
52
+ return_value={
53
+ "1.2.3.4": "aa:bb:cc:dd:ee:ff",
54
+ "4.5.5.6": "ff:bb:cc:0d:ee:ff",
55
+ },
56
+ ),
57
+ patch(
58
+ "aiodiscover.network.get_network",
59
+ return_value=IPv4Network("1.2.3.0/24", False),
60
+ ),
61
+ ):
62
+ hosts = await discover_hosts.async_discover()
63
+
64
+ assert hosts == [
65
+ {"hostname": "router", "ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"},
66
+ {"hostname": "any", "ip": "4.5.5.6", "macaddress": "ff:bb:cc:0d:ee:ff"},
67
+ ]
68
+
69
+
70
+ @pytest.mark.asyncio
71
+ async def test_async_query_for_ptrs() -> None:
72
+ """Test async_query_for_ptrs handles missing ips."""
73
+ loop = asyncio.get_running_loop()
74
+ count = 0
75
+
76
+ @dataclass
77
+ class MockReply:
78
+ name: str
79
+
80
+ def mock_query(*args: Any, **kwargs: Any) -> Any:
81
+ nonlocal count
82
+ count += 1
83
+ future = loop.create_future()
84
+ if count == 2:
85
+ future.set_exception(Exception("test"))
86
+ else:
87
+ future.set_result(MockReply(name=f"name{count}"))
88
+ return future
89
+
90
+ with (
91
+ patch.object(discovery, "DNS_RESPONSE_TIMEOUT", 0),
92
+ patch("aiodiscover.discovery.DNSResolver.query", mock_query),
93
+ ):
94
+ response = await discovery.async_query_for_ptrs(
95
+ "192.168.107.1",
96
+ [
97
+ IPv4Address("192.168.107.2"),
98
+ IPv4Address("192.168.107.3"),
99
+ IPv4Address("192.168.107.4"),
100
+ ],
101
+ )
102
+
103
+ assert len(response) == 3
104
+ assert response[0].name == "name1" # type: ignore
105
+ assert response[1] is None # type: ignore
106
+ assert response[2].name == "name3" # type: ignore
107
+
108
+
109
+ @pytest.mark.asyncio
110
+ async def test_async_query_for_ptrs_chunked() -> None:
111
+ """Test async_query_for_ptrs chunkeds."""
112
+ loop = asyncio.get_running_loop()
113
+ count = 0
114
+
115
+ @dataclass
116
+ class MockReply:
117
+ name: str
118
+
119
+ def mock_query(*args: Any, **kwargs: Any) -> Any:
120
+ nonlocal count
121
+ count += 1
122
+ future = loop.create_future()
123
+ if count == 2:
124
+ future.set_exception(Exception("test"))
125
+ else:
126
+ future.set_result(MockReply(name=f"name{count}"))
127
+ return future
128
+
129
+ with (
130
+ patch.object(discovery, "DNS_RESPONSE_TIMEOUT", 0),
131
+ patch("aiodiscover.discovery.DNSResolver.query", mock_query),
132
+ patch.object(discovery, "QUERY_BUCKET_SIZE", 1),
133
+ ):
134
+ response = await discovery.async_query_for_ptrs(
135
+ "192.168.107.1",
136
+ [
137
+ IPv4Address("192.168.107.2"),
138
+ IPv4Address("192.168.107.3"),
139
+ IPv4Address("192.168.107.4"),
140
+ ],
141
+ )
142
+
143
+ assert len(response) == 3
144
+ assert response[0].name == "name1" # type: ignore
145
+ assert response[1] is None
146
+ assert response[2].name == "name3" # type: ignore
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env python
2
+
3
+ import aiodiscover
4
+
5
+
6
+ def test_get_module_version() -> None:
7
+ """Verify get_module_version does not throw."""
8
+ assert aiodiscover.get_module_version() == aiodiscover.__version__
aiodiscover/util.py ADDED
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ if sys.version_info[:2] < (3, 11):
6
+ from async_timeout import timeout as asyncio_timeout
7
+ else:
8
+ from asyncio import timeout as asyncio_timeout # noqa: F401
@@ -0,0 +1,15 @@
1
+ Apache Software License 2.0
2
+
3
+ Copyright (c) 2021, J. Nick Koston
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.1
2
+ Name: aiodiscover
3
+ Version: 2.2.0
4
+ Summary: Discover hosts by arp and ptr lookup
5
+ Home-page: https://github.com/uilibs/aiodiscover
6
+ Author: J. Nick Koston
7
+ Author-email: nick@koston.org
8
+ Requires-Python: >=3.9,<4.0
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: Apache Software License
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Dist: aiodns (>=3.1.1)
21
+ Requires-Dist: async_timeout (>=4.0.1)
22
+ Requires-Dist: cached_ipaddress (>=0.2.0)
23
+ Requires-Dist: ifaddr (>0.0.0)
24
+ Requires-Dist: netifaces (>=0.11.0)
25
+ Requires-Dist: pyroute2 (>=0.7.3)
26
+ Project-URL: Bug Tracker, https://github.com/bdraco/aiodiscover/issues
27
+ Project-URL: Changelog, https://github.com/bdraco/aiodiscover/blob/main/CHANGELOG.md
28
+ Project-URL: Documentation, https://aiodiscover.readthedocs.io
29
+ Project-URL: Repository, https://github.com/uilibs/aiodiscover
30
+ Description-Content-Type: text/markdown
31
+
32
+ # Async Host discovery
33
+
34
+ [![Build Status](https://github.com/bdraco/aiodiscover/workflows/Build%20Main/badge.svg)](https://github.com/bdraco/aiodiscover/actions)
35
+ [![Documentation](https://github.com/bdraco/aiodiscover/workflows/Documentation/badge.svg)](https://bdraco.github.io/aiodiscover/)
36
+ [![Code Coverage](https://codecov.io/gh/bdraco/aiodiscover/branch/main/graph/badge.svg)](https://codecov.io/gh/bdraco/aiodiscover)
37
+
38
+ Discover hosts by arp and ptr lookup
39
+
40
+ ---
41
+
42
+ ## Features
43
+
44
+ - Discover hosts on the network via ARP and PTR lookup
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import asyncio
50
+ import pprint
51
+ from aiodiscover import DiscoverHosts
52
+
53
+ discover_hosts = DiscoverHosts()
54
+ hosts = asyncio.run(discover_hosts.async_discover())
55
+ pprint.pprint(hosts)
56
+ ```
57
+
58
+ ## Installation
59
+
60
+ **Stable Release:** `pip install aiodiscover`<br>
61
+ **Development Head:** `pip install git+https://github.com/bdraco/aiodiscover.git`
62
+
63
+ ## Documentation
64
+
65
+ For full package documentation please visit [bdraco.github.io/aiodiscover](https://bdraco.github.io/aiodiscover).
66
+
67
+ ## Development
68
+
69
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for information related to developing the code.
70
+
71
+ ## The Four Commands You Need To Know
72
+
73
+ 1. `pip install -e .[dev]`
74
+
75
+ This will install your package in editable mode with all the required development
76
+ dependencies (i.e. `tox`).
77
+
78
+ 2. `make build`
79
+
80
+ This will run `tox` which will run all your tests in both Python 3.7
81
+ and Python 3.8 as well as linting your code.
82
+
83
+ 3. `make clean`
84
+
85
+ This will clean up various Python and build generated files so that you can ensure
86
+ that you are working in a clean environment.
87
+
88
+ 4. `make docs`
89
+
90
+ This will generate and launch a web browser to view the most up-to-date
91
+ documentation for your Python package.
92
+
93
+ #### Additional Optional Setup Steps:
94
+
95
+ - Turn your project into a GitHub repository:
96
+ - Make an account on [github.com](https://github.com)
97
+ - Go to [make a new repository](https://github.com/new)
98
+ - _Recommendations:_
99
+ - _It is strongly recommended to make the repository name the same as the Python
100
+ package name_
101
+ - _A lot of the following optional steps are *free* if the repository is Public,
102
+ plus open source is cool_
103
+ - After a GitHub repo has been created, run the commands listed under:
104
+ "...or push an existing repository from the command line"
105
+ - Register your project with Codecov:
106
+ - Make an account on [codecov.io](https://codecov.io)(Recommended to sign in with GitHub)
107
+ everything else will be handled for you.
108
+ - Ensure that you have set GitHub pages to build the `gh-pages` branch by selecting the
109
+ `gh-pages` branch in the dropdown in the "GitHub Pages" section of the repository settings.
110
+ ([Repo Settings](https://github.com/bdraco/aiodiscover/settings))
111
+ - Register your project with PyPI:
112
+ - Make an account on [pypi.org](https://pypi.org)
113
+ - Go to your GitHub repository's settings and under the
114
+ [Secrets tab](https://github.com/bdraco/aiodiscover/settings/secrets/actions),
115
+ add a secret called `PYPI_TOKEN` with your password for your PyPI account.
116
+ Don't worry, no one will see this password because it will be encrypted.
117
+ - Next time you push to the branch `main` after using `bump2version`, GitHub
118
+ actions will build and deploy your Python package to PyPI.
119
+
120
+ #### Suggested Git Branch Strategy
121
+
122
+ 1. `main` is for the most up-to-date development, very rarely should you directly
123
+ commit to this branch. GitHub Actions will run on every push and on a CRON to this
124
+ branch but still recommended to commit to your development branches and make pull
125
+ requests to main. If you push a tagged commit with bumpversion, this will also release to PyPI.
126
+ 2. Your day-to-day work should exist on branches separate from `main`. Even if it is
127
+ just yourself working on the repository, make a PR from your working branch to `main`
128
+ so that you can ensure your commits don't break the development head. GitHub Actions
129
+ will run on every push to any branch or any pull request from any branch to any other
130
+ branch.
131
+ 3. It is recommended to use "Squash and Merge" commits when committing PR's. It makes
132
+ each set of changes to `main` atomic and as a side effect naturally encourages small
133
+ well defined PR's.
134
+
135
+ **Apache Software License 2.0**
136
+
@@ -0,0 +1,12 @@
1
+ aiodiscover/__init__.py,sha256=6vpH6FNQRJyhHExxDYDMv5e1dYZoIgxLV4MB-dt357g,331
2
+ aiodiscover/discovery.py,sha256=x-G54ilyP5jmF17wZ-ZTvgCrlPVyH8JjEinUibHE4co,5608
3
+ aiodiscover/network.py,sha256=-Tj7FK5uDrhAWwdnYchF6s2x5BnfcmcujvEAmutWQrg,8486
4
+ aiodiscover/tests/__init__.py,sha256=muJYZG2c0S4gX2epuq-XqFa4x2bU2pkRU84m24p63is,41
5
+ aiodiscover/tests/conftest.py,sha256=-JIt3g1eBf4bVQNNQd4JBeQNA7o0LK0W9tYKKX_r5s8,22
6
+ aiodiscover/tests/test_discovery.py,sha256=zNpAKIIcCKTdMXfzczE3VC3aQbxfhpSCVHLAd-g2IgI,4461
7
+ aiodiscover/tests/test_init.py,sha256=UKj1UfbDaeQ1m47P8-AXObkT35SCpHtevM_My0mvDg0,206
8
+ aiodiscover/util.py,sha256=zDWuAJhWkZtz_J0IqpE6sGFJXzjViGxQJKUfdjHogdY,211
9
+ aiodiscover-2.2.0.dist-info/LICENSE,sha256=CRuitFRo3zhJ7rtke-w2aMSDaveQ0nEURZfVd_rFDs0,585
10
+ aiodiscover-2.2.0.dist-info/METADATA,sha256=M3Ioccqhi6bmstFbYqUDhWqrZsgLl_rHfsQn6EXUfMU,5571
11
+ aiodiscover-2.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
12
+ aiodiscover-2.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any