netimps 0.0.2__tar.gz → 0.2.0__tar.gz
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.
- {netimps-0.0.2 → netimps-0.2.0}/AGENTS.md +10 -9
- {netimps-0.0.2 → netimps-0.2.0}/CHANGELOG.md +71 -1
- {netimps-0.0.2 → netimps-0.2.0}/PKG-INFO +34 -15
- {netimps-0.0.2 → netimps-0.2.0}/README.md +29 -13
- {netimps-0.0.2 → netimps-0.2.0}/pyproject.toml +11 -3
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/AGENTS.md +120 -18
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/__init__.py +68 -28
- netimps-0.2.0/src/netimps/_dns.py +678 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_iface_spec.py +2 -2
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_ifaddrs.py +14 -3
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_ip.py +42 -4
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_sockets.py +125 -21
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/cli.py +6 -1
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_centralized.py +170 -7
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_ip.py +42 -1
- netimps-0.2.0/tests/test_net.py +1086 -0
- netimps-0.2.0/tests/typing/api.py +92 -0
- netimps-0.0.2/src/netimps/_dns.py +0 -160
- netimps-0.0.2/tests/test_net.py +0 -487
- {netimps-0.0.2 → netimps-0.2.0}/.gitignore +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/LICENSE +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/docs/api/reference.md +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/docs/changelog.md +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/docs/index.md +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/mkdocs.yml +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/__main__.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_mac.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_multicast.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_ping.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_retry.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_scan.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_scheme.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/_udp.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/src/netimps/py.typed +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_cli.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_interfaces.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_mac.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_scan.py +0 -0
- {netimps-0.0.2 → netimps-0.2.0}/tests/test_sockets.py +0 -0
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
A **small, self-contained network-utilities library** — a thin, typed layer over
|
|
4
4
|
the standard library's `ipaddress`, native cross-platform interface discovery,
|
|
5
|
-
and a handful of host helpers (DNS lookup, ping). One flat import surface,
|
|
6
|
-
runtime
|
|
7
|
-
|
|
5
|
+
and a handful of host helpers (DNS lookup, ping). One flat import surface, no
|
|
6
|
+
hard runtime dependencies (`resolve()` chains `dnspython`/OS-resolver/
|
|
7
|
+
`nslookup` backends, using whichever is available -- `dnspython` is optional,
|
|
8
|
+
via the `dns` extra), and behaviour that stays faithful to the stdlib.
|
|
8
9
|
|
|
9
10
|
```python
|
|
10
11
|
import netimps
|
|
@@ -46,11 +47,11 @@ netimps.ping("8.8.8.8").rtt_ms # 9.0
|
|
|
46
47
|
## Install
|
|
47
48
|
|
|
48
49
|
```bash
|
|
49
|
-
pip install netimps
|
|
50
|
+
pip install netimps # no hard dependencies
|
|
51
|
+
pip install netimps[dns] # plus dnspython, for resolve()'s fullest backend
|
|
50
52
|
```
|
|
51
53
|
|
|
52
|
-
Requires Python 3.9+.
|
|
53
|
-
`resolve`).
|
|
54
|
+
Requires Python 3.9+. No hard runtime dependencies.
|
|
54
55
|
|
|
55
56
|
> **This file is development documentation** — layout, testing, CI, release.
|
|
56
57
|
> It is deliberately **not shipped** in the wheel. The library-usage reference
|
|
@@ -71,7 +72,7 @@ src/netimps/
|
|
|
71
72
|
├── _multicast.py # private: group membership and socket setup
|
|
72
73
|
├── _ifaddrs.py # private: ctypes getifaddrs/GetAdaptersAddresses bindings
|
|
73
74
|
├── _sockets.py # private: source IP, free port, tcp/wait, route, hops, MTU
|
|
74
|
-
├── _dns.py # private: resolve()
|
|
75
|
+
├── _dns.py # private: resolve() chaining dnspython/system/nslookup backends
|
|
75
76
|
├── _ping.py # private: ping() over the platform binary
|
|
76
77
|
└── py.typed # PEP 561 marker — the package ships inline type hints
|
|
77
78
|
```
|
|
@@ -94,7 +95,7 @@ map:
|
|
|
94
95
|
| Name | Purpose |
|
|
95
96
|
| --- | --- |
|
|
96
97
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
97
|
-
| `IPAddressLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
98
|
+
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
98
99
|
| `IPv4Address`, `IPv4Interface`, `IPv4Network`, `IPv6Address`, `IPv6Interface`, `IPv6Network` | stdlib concrete-type re-exports |
|
|
99
100
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
100
101
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -105,7 +106,7 @@ map:
|
|
|
105
106
|
| `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
|
|
106
107
|
| `resolve` | DNS lookup → native records (`[]` on failure) |
|
|
107
108
|
| `ping`, `PingResult` | reachability with RTT and TTL |
|
|
108
|
-
| `bind`, `bind_error_hint`, `interface_for` | socket creation and
|
|
109
|
+
| `bind`, `bind_error_hint`, `interface_for`, `interfaces_for`, `is_local_address` | socket creation and local membership |
|
|
109
110
|
| `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
|
|
110
111
|
| `UdpEndpoint`, `Datagram` | UDP receive with arrival interface (`IP_PKTINFO`) |
|
|
111
112
|
| `Host` | hostname-or-address value type |
|
|
@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.2.0] - 2026-07-29
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Three independently callable DNS backends**, plus `resolve()` chaining
|
|
15
|
+
them: `resolve_dnspython()` (the original `dnspython`-backed
|
|
16
|
+
implementation, every record type), `resolve_system()`
|
|
17
|
+
(`socket.getaddrinfo()` -- hosts file, NSS, OS resolver cache, address
|
|
18
|
+
records only), and `resolve_nslookup()` (shells out to `nslookup`, parses
|
|
19
|
+
both BIND-style and Windows-style output, address records only). `resolve()`
|
|
20
|
+
now tries `["dnspython", "system", "nslookup"]` in order by default and
|
|
21
|
+
returns the first definitive answer, skipping/falling through backends that
|
|
22
|
+
can't serve the request (non-address `rdtype`, missing binary, `dnspython`
|
|
23
|
+
not installed). A custom order/subset is available via
|
|
24
|
+
`resolve(..., backends=[...])` (or a single name as a plain string).
|
|
25
|
+
- `resolve()` (and all three backends) gain a `search` parameter for the
|
|
26
|
+
system resolver's search list (`resolv.conf`'s `search`/`domain`
|
|
27
|
+
directive, or the Windows per-adapter DNS suffix list). It defaults to
|
|
28
|
+
`True`, so an unqualified name like `resolve("db1")` is expanded the way
|
|
29
|
+
`ping db1` would be; `search=False` looks the name up literally (as a
|
|
30
|
+
fully-qualified name, so the OS resolver's own search-list logic doesn't
|
|
31
|
+
kick in either), and a list of domain names tries exactly those suffixes
|
|
32
|
+
instead of the system list.
|
|
33
|
+
- `dnspython` is now an **optional** dependency (`pip install netimps[dns]`),
|
|
34
|
+
since `resolve()` can fall back to `resolve_system()`/`resolve_nslookup()`
|
|
35
|
+
without it. The CLI's `resolve` subcommand now reports a missing-backend
|
|
36
|
+
failure as a clean CLI error rather than an uncaught exception.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **`resolve()`'s default behavior for unqualified names.** Previously an
|
|
41
|
+
unqualified `query` was only ever looked up as-is; it now also tries the
|
|
42
|
+
system resolver's search list first (see `search` above). Pass
|
|
43
|
+
`search=False` to keep the old literal-only behavior. `ns=None` already
|
|
44
|
+
used the system resolver's nameservers; an invalid `ns=` now raises before
|
|
45
|
+
any query is attempted rather than silently falling back to the system
|
|
46
|
+
default.
|
|
47
|
+
- **`resolve()` is no longer purely `dnspython`-backed.** Behavior should be
|
|
48
|
+
unchanged for existing callers when `dnspython` is installed (it's still
|
|
49
|
+
tried first), but a lookup that previously raised or returned `[]` because
|
|
50
|
+
`dnspython` failed for a reason unrelated to the DNS answer itself (e.g. a
|
|
51
|
+
malformed system resolver config) may now succeed via the `system` or
|
|
52
|
+
`nslookup` fallback instead.
|
|
53
|
+
|
|
54
|
+
## [0.1.0] - 2026-07-25
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- **Complete local-interface membership lookups.** `interfaces_for()` yields
|
|
59
|
+
every adapter matching an interface, exact address/`IPInterface`, network,
|
|
60
|
+
or `MACAddress`, while `interface_for()` keeps the first-match scalar
|
|
61
|
+
contract. `is_local_address()` distinguishes an assigned or loopback address
|
|
62
|
+
from one that is merely private, link-local, on-link or reachable.
|
|
63
|
+
- **Static parser contracts.** `TypeForm` overloads preserve union and concrete
|
|
64
|
+
result types, callable builders, and explicit `try_parse(default=...)`
|
|
65
|
+
fallbacks. `IPInterfaceLike` now complements the existing input aliases.
|
|
66
|
+
|
|
67
|
+
### Changed
|
|
68
|
+
|
|
69
|
+
- `interface_for()` accepts networks and MAC addresses, including MAC text and
|
|
70
|
+
6-byte packed values. Integer MACs remain explicit `MACAddress` values. Its legacy
|
|
71
|
+
`strict=False` synthetic fallback remains address-only because a missing
|
|
72
|
+
network or MAC has no honest single-interface representation.
|
|
73
|
+
- The IP input aliases now include packed bytes plus the exact stdlib
|
|
74
|
+
two-tuple and existing-interface forms accepted by the interface/network
|
|
75
|
+
factories. `is_valid()` is documented as a boolean convertibility check
|
|
76
|
+
rather than an unsound type guard for the original object.
|
|
77
|
+
|
|
10
78
|
## [0.0.2] - 2026-07-23
|
|
11
79
|
|
|
12
80
|
### Added
|
|
@@ -87,6 +155,8 @@ below is simply what the package contains.
|
|
|
87
155
|
- **`Host`**, **`retry()`/`backoff_delays()`**, and the named networks `APIPA`,
|
|
88
156
|
`LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6`.
|
|
89
157
|
|
|
90
|
-
[Unreleased]: https://github.com/jose-pr/netimps/compare/v0.0
|
|
158
|
+
[Unreleased]: https://github.com/jose-pr/netimps/compare/v0.1.0...HEAD
|
|
159
|
+
[0.1.0]: https://github.com/jose-pr/netimps/compare/v0.0.2...v0.1.0
|
|
160
|
+
[0.0.2]: https://github.com/jose-pr/netimps/releases/tag/v0.0.2
|
|
91
161
|
[0.0.1]: https://github.com/jose-pr/netimps/releases/tag/v0.0.1
|
|
92
162
|
[0.0.0]: https://github.com/jose-pr/netimps/releases/tag/v0.0.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: netimps
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/netimps
|
|
6
6
|
Project-URL: Documentation, https://jose-pr.github.io/netimps/
|
|
@@ -19,16 +19,19 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Topic :: System :: Networking
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.9
|
|
22
|
-
Requires-Dist: dnspython
|
|
23
22
|
Provides-Extra: cli
|
|
24
23
|
Requires-Dist: duho>=0.3.3; extra == 'cli'
|
|
25
24
|
Provides-Extra: dev
|
|
26
25
|
Requires-Dist: black; extra == 'dev'
|
|
27
26
|
Requires-Dist: build; extra == 'dev'
|
|
27
|
+
Requires-Dist: dnspython; extra == 'dev'
|
|
28
28
|
Requires-Dist: duho>=0.3.3; extra == 'dev'
|
|
29
29
|
Requires-Dist: hatchling; extra == 'dev'
|
|
30
|
+
Requires-Dist: mypy<2; extra == 'dev'
|
|
30
31
|
Requires-Dist: pytest; extra == 'dev'
|
|
31
32
|
Requires-Dist: twine; extra == 'dev'
|
|
33
|
+
Provides-Extra: dns
|
|
34
|
+
Requires-Dist: dnspython; extra == 'dns'
|
|
32
35
|
Provides-Extra: docs
|
|
33
36
|
Requires-Dist: mkdocs; extra == 'docs'
|
|
34
37
|
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
@@ -47,10 +50,13 @@ Description-Content-Type: text/markdown
|
|
|
47
50
|
"which is my IP", reachability checks, CIDR maths, host:port parsing, DNS,
|
|
48
51
|
ping, scanning and multicast — as one typed, flat-import library.
|
|
49
52
|
|
|
50
|
-
Built on the standard library
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
Built on the standard library, with **no hard runtime dependencies**.
|
|
54
|
+
`resolve()` tries `dnspython`, the OS resolver and `nslookup`, in that order,
|
|
55
|
+
using whichever is available (`pip install netimps[dns]` for `dnspython`
|
|
56
|
+
itself, the fullest of the three: structured records, every record type,
|
|
57
|
+
explicit nameserver/search-list control). Interface enumeration, routing and
|
|
58
|
+
multicast are `ctypes` bindings to the platform's own APIs, so there is
|
|
59
|
+
nothing to compile and no wheel to miss for your platform.
|
|
54
60
|
|
|
55
61
|
## Features
|
|
56
62
|
|
|
@@ -58,13 +64,16 @@ and no wheel to miss for your platform.
|
|
|
58
64
|
names, MACs, MTU and **real prefix lengths** on Linux, macOS/BSD and Windows,
|
|
59
65
|
via `getifaddrs(3)` / `GetAdaptersAddresses`. No `ifaddr` required.
|
|
60
66
|
- **One parsing entry point** — `parse(value, type)` with non-raising
|
|
61
|
-
`try_parse` and boolean `is_valid` siblings
|
|
62
|
-
|
|
67
|
+
`try_parse` and boolean `is_valid` siblings. Type-form overloads preserve the
|
|
68
|
+
parsed result, including v4/v6 union aliases and caller-supplied defaults.
|
|
63
69
|
- **`MACAddress`** — colon/hyphen/dot/bare plus `int`/`bytes`, hashable and
|
|
64
70
|
ordered, with `.oui`, `.is_multicast`, `.is_local` and case-selectable
|
|
65
71
|
rendering.
|
|
66
72
|
- **The socket helpers everyone rewrites** — `get_source_ip`, `get_free_port`,
|
|
67
73
|
`tcp_check`, `wait_for_port`.
|
|
74
|
+
- **Local membership lookups** — `interface_for()` gives the first adapter for
|
|
75
|
+
an address, interface, network or MAC; `interfaces_for()` yields every match,
|
|
76
|
+
and `is_local_address()` distinguishes assignment from mere address scope.
|
|
68
77
|
- **Routing and MTU** — `get_route` (first hop, unprivileged), `hop_count`
|
|
69
78
|
(raw sockets *or* traceroute fallback), `discover_mtu` / `get_pmtu`, `Interface.mtu`.
|
|
70
79
|
- **CIDR set maths** — `collapse` and `subtract`, the latter missing from
|
|
@@ -76,18 +85,20 @@ and no wheel to miss for your platform.
|
|
|
76
85
|
`retry()` for the backoff loop everyone writes without jitter.
|
|
77
86
|
- **Multicast** — `multicast_socket` handling the join dance whose failure
|
|
78
87
|
modes are otherwise silent.
|
|
79
|
-
- **DNS and ping** — `resolve()`
|
|
80
|
-
round-trip time and
|
|
88
|
+
- **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
|
|
89
|
+
backends and returning native types; `ping()` returning round-trip time and
|
|
90
|
+
TTL, not just a boolean.
|
|
81
91
|
|
|
82
92
|
## Installation
|
|
83
93
|
|
|
84
94
|
```bash
|
|
85
|
-
pip install netimps # the library
|
|
95
|
+
pip install netimps # the library -- no hard dependencies
|
|
96
|
+
pip install netimps[dns] # plus dnspython, for resolve()'s fullest backend
|
|
86
97
|
pip install netimps[cli] # plus the `netimps` command
|
|
87
98
|
```
|
|
88
99
|
|
|
89
|
-
Requires Python 3.9+.
|
|
90
|
-
requires
|
|
100
|
+
Requires Python 3.9+. Both extras are additive and independent; importing the
|
|
101
|
+
library never requires either.
|
|
91
102
|
|
|
92
103
|
## Command line
|
|
93
104
|
|
|
@@ -128,6 +139,11 @@ netimps.is_valid("::1", IPAddress) # True
|
|
|
128
139
|
netimps.get_source_ip("8.8.8.8") # IPv4Address('192.0.2.10')
|
|
129
140
|
netimps.get_route("8.8.8.8").gateway # IPv4Address('192.0.2.1')
|
|
130
141
|
|
|
142
|
+
# Exact assignment, subnet membership, or MAC ownership
|
|
143
|
+
netimps.interface_for("192.0.2.10") # first matching Interface, or None
|
|
144
|
+
list(netimps.interfaces_for(parse("192.0.2.0/24", IPNetwork)))
|
|
145
|
+
netimps.is_local_address("127.0.0.1") # True without interface discovery
|
|
146
|
+
|
|
131
147
|
# Honest reachability, and waiting for a service
|
|
132
148
|
netimps.tcp_check("example.com", 443) # True
|
|
133
149
|
netimps.wait_for_port("localhost", 5432, timeout=60)
|
|
@@ -176,7 +192,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
176
192
|
| Name | Purpose |
|
|
177
193
|
| --- | --- |
|
|
178
194
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
179
|
-
| `IPAddressLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
195
|
+
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
180
196
|
| `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
|
|
181
197
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
182
198
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -187,7 +203,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
187
203
|
| `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
|
|
188
204
|
| `resolve` | DNS lookup → native records (`[]` on failure) |
|
|
189
205
|
| `ping`, `PingResult` | reachability with RTT and TTL |
|
|
190
|
-
| `bind`, `bind_error_hint`, `interface_for` | socket creation and
|
|
206
|
+
| `bind`, `bind_error_hint`, `interface_for`, `interfaces_for`, `is_local_address` | socket creation and local membership |
|
|
191
207
|
| `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
|
|
192
208
|
| `UdpEndpoint`, `Datagram` | UDP receive with arrival interface (`IP_PKTINFO`) |
|
|
193
209
|
| `Host` | hostname-or-address value type |
|
|
@@ -212,6 +228,9 @@ A few behaviours are deliberate and worth knowing:
|
|
|
212
228
|
`parse("::1", IPv4Address)` raises rather than quietly returning v6.
|
|
213
229
|
- **Networks parse non-strict by default**, so `10.0.0.5/24` normalises instead
|
|
214
230
|
of raising. Pass `strict=True` for stdlib behaviour.
|
|
231
|
+
- **Addresses can belong to more than one interface.** `interface_for()` keeps
|
|
232
|
+
the historical first-match result; use `interfaces_for()` when duplicates
|
|
233
|
+
such as unscoped IPv6 link-local assignments matter.
|
|
215
234
|
- **`resolve` raises on a malformed query** rather than returning `[]` — a
|
|
216
235
|
typo'd record type should not look like "no such record".
|
|
217
236
|
- **`ping(ttl=...)` behaves the same on every OS.** Windows `ping` exits `0`
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
"which is my IP", reachability checks, CIDR maths, host:port parsing, DNS,
|
|
11
11
|
ping, scanning and multicast — as one typed, flat-import library.
|
|
12
12
|
|
|
13
|
-
Built on the standard library
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
Built on the standard library, with **no hard runtime dependencies**.
|
|
14
|
+
`resolve()` tries `dnspython`, the OS resolver and `nslookup`, in that order,
|
|
15
|
+
using whichever is available (`pip install netimps[dns]` for `dnspython`
|
|
16
|
+
itself, the fullest of the three: structured records, every record type,
|
|
17
|
+
explicit nameserver/search-list control). Interface enumeration, routing and
|
|
18
|
+
multicast are `ctypes` bindings to the platform's own APIs, so there is
|
|
19
|
+
nothing to compile and no wheel to miss for your platform.
|
|
17
20
|
|
|
18
21
|
## Features
|
|
19
22
|
|
|
@@ -21,13 +24,16 @@ and no wheel to miss for your platform.
|
|
|
21
24
|
names, MACs, MTU and **real prefix lengths** on Linux, macOS/BSD and Windows,
|
|
22
25
|
via `getifaddrs(3)` / `GetAdaptersAddresses`. No `ifaddr` required.
|
|
23
26
|
- **One parsing entry point** — `parse(value, type)` with non-raising
|
|
24
|
-
`try_parse` and boolean `is_valid` siblings
|
|
25
|
-
|
|
27
|
+
`try_parse` and boolean `is_valid` siblings. Type-form overloads preserve the
|
|
28
|
+
parsed result, including v4/v6 union aliases and caller-supplied defaults.
|
|
26
29
|
- **`MACAddress`** — colon/hyphen/dot/bare plus `int`/`bytes`, hashable and
|
|
27
30
|
ordered, with `.oui`, `.is_multicast`, `.is_local` and case-selectable
|
|
28
31
|
rendering.
|
|
29
32
|
- **The socket helpers everyone rewrites** — `get_source_ip`, `get_free_port`,
|
|
30
33
|
`tcp_check`, `wait_for_port`.
|
|
34
|
+
- **Local membership lookups** — `interface_for()` gives the first adapter for
|
|
35
|
+
an address, interface, network or MAC; `interfaces_for()` yields every match,
|
|
36
|
+
and `is_local_address()` distinguishes assignment from mere address scope.
|
|
31
37
|
- **Routing and MTU** — `get_route` (first hop, unprivileged), `hop_count`
|
|
32
38
|
(raw sockets *or* traceroute fallback), `discover_mtu` / `get_pmtu`, `Interface.mtu`.
|
|
33
39
|
- **CIDR set maths** — `collapse` and `subtract`, the latter missing from
|
|
@@ -39,18 +45,20 @@ and no wheel to miss for your platform.
|
|
|
39
45
|
`retry()` for the backoff loop everyone writes without jitter.
|
|
40
46
|
- **Multicast** — `multicast_socket` handling the join dance whose failure
|
|
41
47
|
modes are otherwise silent.
|
|
42
|
-
- **DNS and ping** — `resolve()`
|
|
43
|
-
round-trip time and
|
|
48
|
+
- **DNS and ping** — `resolve()` chaining `dnspython`/OS resolver/`nslookup`
|
|
49
|
+
backends and returning native types; `ping()` returning round-trip time and
|
|
50
|
+
TTL, not just a boolean.
|
|
44
51
|
|
|
45
52
|
## Installation
|
|
46
53
|
|
|
47
54
|
```bash
|
|
48
|
-
pip install netimps # the library
|
|
55
|
+
pip install netimps # the library -- no hard dependencies
|
|
56
|
+
pip install netimps[dns] # plus dnspython, for resolve()'s fullest backend
|
|
49
57
|
pip install netimps[cli] # plus the `netimps` command
|
|
50
58
|
```
|
|
51
59
|
|
|
52
|
-
Requires Python 3.9+.
|
|
53
|
-
requires
|
|
60
|
+
Requires Python 3.9+. Both extras are additive and independent; importing the
|
|
61
|
+
library never requires either.
|
|
54
62
|
|
|
55
63
|
## Command line
|
|
56
64
|
|
|
@@ -91,6 +99,11 @@ netimps.is_valid("::1", IPAddress) # True
|
|
|
91
99
|
netimps.get_source_ip("8.8.8.8") # IPv4Address('192.0.2.10')
|
|
92
100
|
netimps.get_route("8.8.8.8").gateway # IPv4Address('192.0.2.1')
|
|
93
101
|
|
|
102
|
+
# Exact assignment, subnet membership, or MAC ownership
|
|
103
|
+
netimps.interface_for("192.0.2.10") # first matching Interface, or None
|
|
104
|
+
list(netimps.interfaces_for(parse("192.0.2.0/24", IPNetwork)))
|
|
105
|
+
netimps.is_local_address("127.0.0.1") # True without interface discovery
|
|
106
|
+
|
|
94
107
|
# Honest reachability, and waiting for a service
|
|
95
108
|
netimps.tcp_check("example.com", 443) # True
|
|
96
109
|
netimps.wait_for_port("localhost", 5432, timeout=60)
|
|
@@ -139,7 +152,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
139
152
|
| Name | Purpose |
|
|
140
153
|
| --- | --- |
|
|
141
154
|
| `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
|
|
142
|
-
| `IPAddressLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
155
|
+
| `IPAddressLike`, `IPInterfaceLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
|
|
143
156
|
| `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
|
|
144
157
|
| `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
|
|
145
158
|
| `MACAddress` | parse / classify / render MAC addresses |
|
|
@@ -150,7 +163,7 @@ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
|
|
|
150
163
|
| `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
|
|
151
164
|
| `resolve` | DNS lookup → native records (`[]` on failure) |
|
|
152
165
|
| `ping`, `PingResult` | reachability with RTT and TTL |
|
|
153
|
-
| `bind`, `bind_error_hint`, `interface_for` | socket creation and
|
|
166
|
+
| `bind`, `bind_error_hint`, `interface_for`, `interfaces_for`, `is_local_address` | socket creation and local membership |
|
|
154
167
|
| `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
|
|
155
168
|
| `UdpEndpoint`, `Datagram` | UDP receive with arrival interface (`IP_PKTINFO`) |
|
|
156
169
|
| `Host` | hostname-or-address value type |
|
|
@@ -175,6 +188,9 @@ A few behaviours are deliberate and worth knowing:
|
|
|
175
188
|
`parse("::1", IPv4Address)` raises rather than quietly returning v6.
|
|
176
189
|
- **Networks parse non-strict by default**, so `10.0.0.5/24` normalises instead
|
|
177
190
|
of raising. Pass `strict=True` for stdlib behaviour.
|
|
191
|
+
- **Addresses can belong to more than one interface.** `interface_for()` keeps
|
|
192
|
+
the historical first-match result; use `interfaces_for()` when duplicates
|
|
193
|
+
such as unscoped IPv6 link-local assignments matter.
|
|
178
194
|
- **`resolve` raises on a malformed query** rather than returning `[]` — a
|
|
179
195
|
typo'd record type should not look like "no such record".
|
|
180
196
|
- **`ping(ttl=...)` behaves the same on every OS.** Windows `ping` exits `0`
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netimps"
|
|
7
|
-
version = "0.0
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
authors = [{ name = "Jose A." }]
|
|
9
9
|
description = "Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery"
|
|
10
10
|
readme = "README.md"
|
|
@@ -23,12 +23,16 @@ classifiers = [
|
|
|
23
23
|
"Topic :: System :: Networking",
|
|
24
24
|
"Typing :: Typed",
|
|
25
25
|
]
|
|
26
|
-
dependencies = [
|
|
26
|
+
dependencies = []
|
|
27
27
|
|
|
28
28
|
[project.optional-dependencies]
|
|
29
|
+
# dnspython backs resolve_dnspython() only -- resolve_system()/resolve_nslookup()
|
|
30
|
+
# (and resolve()'s fallback chain through them) work without it, so it is not a
|
|
31
|
+
# hard dependency. Importing netimps must never require it.
|
|
32
|
+
dns = ['dnspython']
|
|
29
33
|
# The CLI is optional: importing netimps must never require duho.
|
|
30
34
|
cli = ['duho>=0.3.3']
|
|
31
|
-
dev = ['build', 'twine', 'hatchling', 'pytest', 'black', 'duho>=0.3.3']
|
|
35
|
+
dev = ['build', 'twine', 'hatchling', 'pytest', 'black', 'mypy<2', 'duho>=0.3.3', 'dnspython']
|
|
32
36
|
docs = ['mkdocs', 'mkdocs-material', 'mkdocstrings[python]']
|
|
33
37
|
|
|
34
38
|
[project.scripts]
|
|
@@ -57,6 +61,10 @@ exclude = ["/.*"]
|
|
|
57
61
|
# 3.9 is the floor, so don't let black emit syntax newer than that.
|
|
58
62
|
target-version = ["py39"]
|
|
59
63
|
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
python_version = "3.9"
|
|
66
|
+
enable_incomplete_feature = ["TypeForm"]
|
|
67
|
+
|
|
60
68
|
[tool.pytest.ini_options]
|
|
61
69
|
pythonpath = ["src"]
|
|
62
70
|
testpaths = ["tests"]
|
|
@@ -16,7 +16,7 @@ is self-contained: it references nothing outside the installed distribution.
|
|
|
16
16
|
testing, releasing) is not shipped; it lives with the source at
|
|
17
17
|
<https://github.com/jose-pr/netimps>.
|
|
18
18
|
|
|
19
|
-
`netimps.__version__` — the package version string (currently `"0.0
|
|
19
|
+
`netimps.__version__` — the package version string (currently `"0.2.0"`).
|
|
20
20
|
|
|
21
21
|
## Argument naming
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ The package is consistent about what the first argument means:
|
|
|
27
27
|
| `dst` | where traffic is **sent** | `ping`, `tcp_check`, `wait_for_port`, `get_route`, `hop_count`, `discover_mtu` |
|
|
28
28
|
| `src` | where traffic is **sent from** | `ping(src=)`, `get_free_port(src=)`, `discover_mtu(src=)` |
|
|
29
29
|
| `host` / `network` | the thing being **examined** | `scan_ports(host)`, `scan_hosts(network)` |
|
|
30
|
-
| `address` / `ip` | an address being **classified** (no DNS) | `get_ip`, `interface_for`, `is_multicast`, `is_link_scoped` |
|
|
30
|
+
| `address` / `ip` | an address being **classified** (no DNS) | `get_ip`, `interface_for`, `interfaces_for`, `is_local_address`, `is_multicast`, `is_link_scoped` |
|
|
31
31
|
|
|
32
32
|
`dst`/`src` are abbreviated symmetrically, matching packet-header convention.
|
|
33
33
|
A `dst` accepts a hostname; an `address` does not.
|
|
@@ -55,6 +55,7 @@ The union aliases are **not callable** — `IPAddress("10.0.0.5")` is a
|
|
|
55
55
|
| `IPInterface` | `IPv4Interface \| IPv6Interface` (address + prefix) |
|
|
56
56
|
| `IPNetwork` | `IPv4Network \| IPv6Network` |
|
|
57
57
|
| `IPAddressLike` | anything accepted *as input* for an address |
|
|
58
|
+
| `IPInterfaceLike` | anything accepted *as input* for an address + prefix |
|
|
58
59
|
| `IPNetworkLike` | anything accepted *as input* for a network |
|
|
59
60
|
| `MACLike` | `str \| int \| bytes \| MACAddress` |
|
|
60
61
|
|
|
@@ -80,6 +81,9 @@ keyword. Key behaviours:
|
|
|
80
81
|
- **Every type accepts the full stdlib input range** — `str`, `int`, packed
|
|
81
82
|
`bytes`, or an existing object — because the builders are `ipaddress.ip_*`,
|
|
82
83
|
not the concrete constructors.
|
|
84
|
+
- **`IPInterface` and non-strict `IPNetwork` accept the stdlib two-tuple
|
|
85
|
+
form**, such as `("10.0.0.5", 24)`. `IPNetwork` also accepts an existing
|
|
86
|
+
`IPInterface` and normalises its host bits. `IPAddress` accepts neither.
|
|
83
87
|
- **Unions accept either family; concrete types are strict.**
|
|
84
88
|
`parse("::1", IPAddress)` works; `parse("::1", IPv4Address)` raises, because
|
|
85
89
|
asking for v4 and receiving v6 would defeat the request.
|
|
@@ -91,6 +95,11 @@ keyword. Key behaviours:
|
|
|
91
95
|
than being disguised as a rejected value.
|
|
92
96
|
- An unusable `type` raises `TypeError` **even from `try_parse`** — a caller
|
|
93
97
|
bug is not a rejected value.
|
|
98
|
+
- Static type checkers preserve the selected result type, including the
|
|
99
|
+
`IPAddress`/`IPInterface`/`IPNetwork` unions, concrete classes, callable
|
|
100
|
+
builders, and the union with an explicit `try_parse(default=...)`.
|
|
101
|
+
- **`is_valid` returns a plain `bool`; it does not narrow the original input.**
|
|
102
|
+
It proves convertibility, and parsing can create a different object.
|
|
94
103
|
|
|
95
104
|
> **Gotcha:** `is_valid` uses an internal sentinel rather than testing
|
|
96
105
|
> `try_parse(...) is not None`, so a builder that legitimately returns `None`
|
|
@@ -197,21 +206,97 @@ Where several schemes share a port the **canonical** one is returned (1080 →
|
|
|
197
206
|
|
|
198
207
|
## DNS
|
|
199
208
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
209
|
+
Three independently callable backends, plus `resolve()`, which chains them.
|
|
210
|
+
All four share one contract: a `list`, **empty on any genuine lookup
|
|
211
|
+
failure** (NXDOMAIN, NODATA, timeout) — never `None` — with **native
|
|
212
|
+
types**: `A`/`AAAA` records are `ipaddress` objects, everything else is
|
|
213
|
+
`str` (trailing root dot stripped, TXT strings unquoted).
|
|
214
|
+
|
|
215
|
+
**`resolve(query, rdtype="a", ns=None, timeout=5.0, port=53, tcp=False, search=True, backends=None)`**
|
|
216
|
+
|
|
217
|
+
Tries each backend in `backends` (default `["dnspython", "system",
|
|
218
|
+
"nslookup"]`) until one gives a **definitive** answer — records, or a real
|
|
219
|
+
empty result (NXDOMAIN/NODATA) — and returns that. A backend that could not
|
|
220
|
+
even *attempt* the query (missing binary, `dnspython` not installed, a
|
|
221
|
+
`rdtype`/`ns` it structurally can't serve) is skipped or falls through,
|
|
222
|
+
never mistaken for "no records". If every applicable backend fails that way,
|
|
223
|
+
the last such error is raised. `backends` also accepts a single name as a
|
|
224
|
+
plain string (`backends="system"`), or a custom order/subset
|
|
225
|
+
(`backends=["nslookup", "dnspython"]`).
|
|
226
|
+
|
|
227
|
+
- **`system`** is skipped automatically for a non-address `rdtype` or an
|
|
228
|
+
explicit `ns=`/`port=` — it has no per-call nameserver override, so running
|
|
229
|
+
it anyway would silently ignore the caller's choice.
|
|
230
|
+
- A malformed query or unknown record type raises `ValueError` immediately,
|
|
231
|
+
without trying every backend — that's a caller bug, not a resolution
|
|
232
|
+
outcome.
|
|
233
|
+
|
|
234
|
+
**`resolve_dnspython(query, rdtype="a", ns=None, timeout=5.0, port=53, tcp=False, search=True)`**
|
|
235
|
+
|
|
236
|
+
The original backend: `dnspython`, structured records, every `rdtype`.
|
|
237
|
+
|
|
238
|
+
- **`ns=None` (default) uses the system resolver configuration** —
|
|
239
|
+
`/etc/resolv.conf` on POSIX, the registry on Windows. Pass `ns=` (a string
|
|
240
|
+
or list) to query specific nameservers instead; a malformed `ns=` raises
|
|
241
|
+
immediately, before any query is attempted.
|
|
242
|
+
- **`search=True` (default) tries the resolver's search list** (the
|
|
243
|
+
`search`/`domain` directive in `resolv.conf`, or the Windows per-adapter DNS
|
|
244
|
+
suffix list) for an unqualified `query` — e.g. `resolve_dnspython("db1")`
|
|
245
|
+
trying `db1.internal.example.com`, the same as `ping db1` would.
|
|
246
|
+
`search=False` looks up `query` literally. A **list of domain names**
|
|
247
|
+
(`search=["eng.example.com", "example.com"]`) tries exactly those suffixes
|
|
248
|
+
instead of the system list, regardless of `ns`. Ignored for an
|
|
249
|
+
already-qualified (trailing-dot) `query`.
|
|
212
250
|
- `timeout` bounds the **whole resolution including retries**, so a list of
|
|
213
251
|
dead nameservers cannot run past it.
|
|
214
|
-
- Requires `dnspython` — the package's only runtime dependency.
|
|
252
|
+
- Requires `dnspython` — the package's only runtime dependency. Raises
|
|
253
|
+
`ResolutionError` (not `ValueError`) if it isn't installed, so `resolve()`'s
|
|
254
|
+
chain falls through to the next backend instead of erroring outright.
|
|
255
|
+
|
|
256
|
+
**`resolve_system(query, rdtype="a", timeout=5.0, search=True)`**
|
|
257
|
+
|
|
258
|
+
The OS resolver, via `socket.getaddrinfo()` — **hosts file, NSS
|
|
259
|
+
(`nsswitch.conf`) and DNS, in the order the OS applies them**, including any
|
|
260
|
+
OS-level resolver cache. This is what `resolve_dnspython` cannot see (its own
|
|
261
|
+
DNS query bypasses all of that).
|
|
262
|
+
|
|
263
|
+
- **Address records only**: `rdtype` must be `"a"` or `"aaaa"`; anything else
|
|
264
|
+
raises `ResolutionError` immediately, no query attempted.
|
|
265
|
+
- **No `ns=` override** — `getaddrinfo` always asks whatever resolver the OS
|
|
266
|
+
is configured with; there's no per-call nameserver parameter at that layer
|
|
267
|
+
(not even via `ctypes` — reaching a specific nameserver without shelling
|
|
268
|
+
out means speaking DNS wire protocol yourself, which is what `dnspython`
|
|
269
|
+
already does).
|
|
270
|
+
- **`search`**: `getaddrinfo` itself takes no search-list parameter either, so
|
|
271
|
+
`search=True` (default) just leaves `query` as given and the OS resolver's
|
|
272
|
+
own configured search list (glibc `ndots`/`search`, Windows per-adapter DNS
|
|
273
|
+
suffix) applies as it normally would. `search=False` appends a trailing
|
|
274
|
+
`.`, which every resolver reads as "already fully qualified" — the same
|
|
275
|
+
trick `host`/`getent` scripts use. A **list of domain names** tries `query`
|
|
276
|
+
qualified with each, in order, one `getaddrinfo` call per candidate,
|
|
277
|
+
independent of (and untouched by) the OS's own search list.
|
|
278
|
+
|
|
279
|
+
**`resolve_nslookup(query, rdtype="a", ns=None, timeout=5.0, search=True)`**
|
|
280
|
+
|
|
281
|
+
Shells out to the `nslookup` binary — a fallback for when neither Python-level
|
|
282
|
+
path is usable. Address records only: `rdtype` must be `"a"`, `"aaaa"` or
|
|
283
|
+
`"ptr"`.
|
|
284
|
+
|
|
285
|
+
- Parses **both BIND-style** (`Address: 1.2.3.4`, one line per address) **and
|
|
286
|
+
Windows-style** (`Addresses:` with continuation lines, and NODATA as a bare
|
|
287
|
+
`Name:` line with no address and no error text) output. Windows also prints
|
|
288
|
+
its NXDOMAIN message on **stderr**, not stdout — both streams are checked.
|
|
289
|
+
- `ns=` is passed as `nslookup`'s trailing `server` argument (a single
|
|
290
|
+
nameserver, not a list).
|
|
291
|
+
- **`search`** has the same three-way contract as the other backends, but
|
|
292
|
+
`nslookup` has no built-in search-list handling — this issues one
|
|
293
|
+
`nslookup` call per candidate name, in order, stopping at the first with
|
|
294
|
+
actual records. `search=True` (default) draws the candidate list from the
|
|
295
|
+
system resolver's search config (reusing `dnspython`'s `resolv.conf`/
|
|
296
|
+
registry parsing if it's installed; `[]` — literal name only — if not).
|
|
297
|
+
- Raises `ResolutionError` (not `ValueError`) for a missing binary, a
|
|
298
|
+
timeout, or an unparseable output shape — a genuine "no such name" is
|
|
299
|
+
still `[]`.
|
|
215
300
|
|
|
216
301
|
## Reachability
|
|
217
302
|
|
|
@@ -257,9 +342,26 @@ failure. `tcp` and `udp` also report `rtt_ms`; only ICMP reports `ttl`.
|
|
|
257
342
|
for a bind failure, recognising POSIX errnos *and* Windows `10013`/`10048`.
|
|
258
343
|
Returns `None` for anything unrecognised, so the caller keeps the original
|
|
259
344
|
error. **Does not raise** — what to do with a failure is the caller's call.
|
|
260
|
-
- **`interface_for(
|
|
261
|
-
|
|
262
|
-
`
|
|
345
|
+
- **`interface_for(query, strict=True) -> Interface | None`** — first matching
|
|
346
|
+
adapter in OS enumeration order. `query` accepts an `Interface`, exact
|
|
347
|
+
`IPAddress`, exact `.ip` from an `IPInterface`, an `IPNetwork` containing at
|
|
348
|
+
least one assigned address, or an exact `MACAddress`. Address-like strings,
|
|
349
|
+
integers and packed bytes remain accepted; a slash-bearing string is a
|
|
350
|
+
network. MAC text and 6-byte packed values are recognised after IP parsing.
|
|
351
|
+
Integer MACs must be wrapped in `MACAddress` because integers are also valid
|
|
352
|
+
IP-address inputs. Invalid input and misses return `None`. With
|
|
353
|
+
`strict=False`, only an address or `IPInterface` miss synthesizes a host-route
|
|
354
|
+
interface named `"<unknown>"`; networks and MACs have no honest synthetic
|
|
355
|
+
result.
|
|
356
|
+
- **`interfaces_for(query) -> Iterator[Interface]`** — every match for the same
|
|
357
|
+
query forms, in OS order and with each adapter yielded once even if several
|
|
358
|
+
assigned addresses match. An `Interface` yields itself without enumeration.
|
|
359
|
+
Addresses need not be unique across adapters (unscoped IPv6 link-local is a
|
|
360
|
+
common example), so use this plural form when every owner matters.
|
|
361
|
+
- **`is_local_address(address) -> bool`** — true only for loopback or an
|
|
362
|
+
address assigned to a local adapter. Private, link-local, on-link, routable
|
|
363
|
+
or reachable alone do not count. Malformed input raises like `parse`;
|
|
364
|
+
loopback answers before interface discovery.
|
|
263
365
|
- **`get_source_ip(dst="8.8.8.8", port=80)`** — which local address the kernel
|
|
264
366
|
would use to reach `dst`. **Sends no packets.** The answer depends on
|
|
265
367
|
`dst`: with a VPN up, a public probe returns the tunnel address and a LAN
|