netimps 0.0.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.
@@ -0,0 +1,38 @@
1
+ # Agent configs / private notes — never tracked in this repo
2
+ .agents
3
+ *.local.md
4
+ CLAUDE*
5
+ .claude/
6
+
7
+ # Virtualenvs
8
+ .venv*/
9
+ venv/
10
+ env/
11
+ .env/
12
+
13
+ # Byte-compiled / optimized
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+
18
+ # Distribution / packaging
19
+ build/
20
+ dist/
21
+ *.egg-info/
22
+ *.egg
23
+ .eggs/
24
+
25
+ # Test / coverage / type-checker caches
26
+ .pytest_cache/
27
+ .mypy_cache/
28
+ .ruff_cache/
29
+ .coverage
30
+ htmlcov/
31
+ .tox/
32
+
33
+ # Editors / OS
34
+ .vscode/
35
+ .idea/
36
+ *.swp
37
+ .DS_Store
38
+ site/
@@ -0,0 +1,181 @@
1
+ # netimps
2
+
3
+ A **small, self-contained network-utilities library** — a thin, typed layer over
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, one
6
+ runtime dependency (`dnspython`, used only by `resolve`), and behaviour that
7
+ stays faithful to the stdlib.
8
+
9
+ ```python
10
+ import netimps
11
+ from netimps import IPNetwork, MACAddress, parse
12
+
13
+ for iface in netimps.get_interfaces():
14
+ print(iface.name, iface.mac, iface.mtu, [str(ip) for ip in iface.ips])
15
+
16
+ parse("10.0.0.5/24", IPNetwork) # IPv4Network('10.0.0.0/24')
17
+ netimps.get_source_ip("8.8.8.8") # the address that actually reaches it
18
+ netimps.tcp_check("example.com", 443) # True
19
+ netimps.resolve("example.com") # [IPv4Address(...)] ([] on failure)
20
+ netimps.ping("8.8.8.8").rtt_ms # 9.0
21
+ ```
22
+
23
+ - **Interface discovery, no dependencies** — `get_interfaces()` gives adapter
24
+ names, MACs, MTU and *real* prefix lengths on Linux, macOS/BSD and Windows,
25
+ via `ctypes` bindings to `getifaddrs(3)` / `GetAdaptersAddresses`. Results are
26
+ normalised across platforms; native leftovers are opt-in via `raw=True`.
27
+ - **One parsing entry point** — `parse(value, type)`, plus non-raising
28
+ `try_parse` and boolean `is_valid`. `IPAddress`/`IPInterface`/`IPNetwork` are
29
+ the v4/v6 unions you annotate with *and* the types you parse into.
30
+ - **`MACAddress`** — colon/hyphen/dot/bare plus `int`/`bytes`, hashable and
31
+ ordered, with `.packed`, `.oui`, `.is_multicast`, `.is_local`,
32
+ `.as_str(sep, upper=)` and `is_valid`/`try_parse` classmethods.
33
+ - **Socket helpers** — `get_source_ip`, `get_free_port`, `tcp_check`,
34
+ `wait_for_port`: the four every network tool rewrites.
35
+ - **Routing and MTU** — `get_route` (first hop, unprivileged), `hop_count`
36
+ (raw sockets or traceroute fallback), `discover_mtu` / `get_pmtu`, `Interface.mtu`.
37
+ - **CIDR maths and host parsing** — `collapse`, `subtract` (absent from
38
+ `ipaddress`), and `normalize_host` with correct IPv6 bracket handling.
39
+ - **Scanning** — concurrent `scan_ports` / `scan_hosts`, ports addressable by
40
+ scheme name.
41
+ - **Multicast** — `multicast_socket`, `join_group`, `leave_group`, wrapping the
42
+ setup whose failure modes are silent.
43
+ - **DNS and ping** — `resolve()` returning native types; `ping()` returning a
44
+ `PingResult` with RTT and TTL that stays truthy.
45
+
46
+ ## Install
47
+
48
+ ```bash
49
+ pip install netimps
50
+ ```
51
+
52
+ Requires Python 3.9+. Runtime dependency: `dnspython` (used only inside
53
+ `resolve`).
54
+
55
+ > **This file is development documentation** — layout, testing, CI, release.
56
+ > It is deliberately **not shipped** in the wheel. The library-usage reference
57
+ > is `src/netimps/AGENTS.md`, which *is* shipped and must stay self-contained
58
+ > (no repo-relative links, since an installed consumer has no repo).
59
+
60
+ ## Code layout
61
+
62
+ ```
63
+ src/netimps/
64
+ ├── __init__.py # the public surface: generic parse/try_parse/is_valid
65
+ ├── _ip.py # private: IP type aliases, builder tables, IP helpers
66
+ ├── _mac.py # private: MACAddress value type
67
+ ├── _scheme.py # private: scheme <-> port registry
68
+ ├── _scan.py # private: concurrent port/host scanning
69
+ ├── _multicast.py # private: group membership and socket setup
70
+ ├── _ifaddrs.py # private: ctypes getifaddrs/GetAdaptersAddresses bindings
71
+ ├── _sockets.py # private: source IP, free port, tcp/wait, route, hops, MTU
72
+ ├── _dns.py # private: resolve() over dnspython
73
+ ├── _ping.py # private: ping() over the platform binary
74
+ └── py.typed # PEP 561 marker — the package ships inline type hints
75
+ ```
76
+
77
+ **The import surface is still flat** — everything is re-exported from
78
+ `netimps`, and the `_`-prefixed modules are implementation detail. Do not
79
+ import them directly from outside the package.
80
+
81
+ `__init__` imports the submodules **last**, because several of them call back
82
+ into it (`parse`, `try_parse`, `MACAddress`); those back-references are
83
+ function-local imports for the same reason. Everything importable from
84
+ `netimps` is declared in `__all__` at the top of `__init__.py`.
85
+
86
+ ## Entry points
87
+
88
+ See **`src/netimps/AGENTS.md`** for the header-file-style public API (every
89
+ export with its signature, arguments, return contract, and gotchas). Quick
90
+ map:
91
+
92
+ | Name | Purpose |
93
+ | --- | --- |
94
+ | `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
95
+ | `IPAddressLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
96
+ | `IPv4Address`, `IPv4Interface`, `IPv4Network`, `IPv6Address`, `IPv6Interface`, `IPv6Network` | stdlib concrete-type re-exports |
97
+ | `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
98
+ | `MACAddress` | parse / classify / render MAC addresses |
99
+ | `get_interfaces`, `Interface`, `iter_addresses` | native cross-platform NIC discovery |
100
+ | `get_ip`, `is_link_scoped` | address resolution and scope classification |
101
+ | `collapse`, `subtract` | CIDR set maths |
102
+ | `normalize_host` | `host:port` splitting, IPv6-aware |
103
+ | `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
104
+ | `resolve` | DNS lookup → native records (`[]` on failure) |
105
+ | `ping`, `PingResult` | reachability with RTT and TTL |
106
+ | `bind`, `bind_error_hint`, `interface_for` | socket creation and diagnosis |
107
+ | `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
108
+ | `UdpEndpoint`, `Datagram` | UDP receive with arrival interface (`IP_PKTINFO`) |
109
+ | `Host` | hostname-or-address value type |
110
+ | `retry`, `backoff_delays` | bounded retry with exponential backoff |
111
+ | `APIPA`, `LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6` | named networks |
112
+ | `get_route`, `Route`, `hop_count` | routing and distance |
113
+ | `discover_mtu`, `get_pmtu`, `get_tcp_mss` | path MTU by ICMP/UDP/TCP, the kernel's cached guess, or the negotiated MSS |
114
+ | `scan_ports`, `scan_hosts`, `PORT_RANGES` | concurrent scanning |
115
+ | `multicast_socket`, `join_group`, `leave_group`, `is_multicast` | multicast |
116
+ | `HOST_DN` | `platform.node()` of the running host, captured at import time |
117
+
118
+ ## Working here
119
+
120
+ - **Don't collapse the per-platform `sockaddr` layouts** in `_ifaddrs.py`.
121
+ macOS/BSD have a leading `sa_len` byte Linux lacks; using the Linux layout on
122
+ BSD decodes `AF_INET` as `512` and *silently* drops every address instead of
123
+ raising — a Linux-only CI stays green while Mac users lose data.
124
+ - **`is_loopback` is derived from addresses, never names** (`lo` / `lo0` /
125
+ `Loopback Pseudo-Interface 1` share no spelling).
126
+ - The ctypes paths can't be asserted against fixed values, so
127
+ `tests/test_interfaces.py` checks invariants plus the pure helpers and the
128
+ fallback, which *are* exactly testable.
129
+ - Tests must never hit the network — `tests/test_net.py` fakes `dns.resolver`
130
+ and `subprocess.run` throughout. `test_scan.py` and `test_sockets.py` use
131
+ loopback only.
132
+ - **`_ip` is imported *before* the definitions** in `__init__`, unlike the other
133
+ submodules which are imported last. `parse()` uses `IPAddress` as a default
134
+ argument, and defaults evaluate at definition time.
135
+ - **Windows `ping` exits 0 for "TTL expired in transit."** Anything inferring
136
+ success from the exit code alone is wrong; match the reply address instead,
137
+ never the localised prose.
138
+ - **Check for silent platform gaps before adding a socket option.** `IP_MTU`,
139
+ `IP_MTU_DISCOVER`, `IP_DONTFRAG` and `SO_REUSEPORT` do not exist on Windows;
140
+ binding a multicast socket to the group address fails there too.
141
+ - **Windows exposes no cached path MTU.** Already investigated, so do not
142
+ re-derive it: `MIB_IPFORWARDROW.dwForwardMtu` reads 0 (unsupported), and
143
+ `MIB_IPFORWARD_ROW2` has no MTU field. `Interface.mtu` is the link MTU;
144
+ `discover_mtu` probing is the only way to get a path MTU there.
145
+ - **`GetBestRoute`, not `GetIpForwardTable`.** It asks Windows which route it
146
+ would pick, so the kernel does longest-prefix matching. The POSIX side has no
147
+ equivalent and must parse `/proc/net/route` by hand — which is where the
148
+ loopback bug came from, since that file omits loopback entirely.
149
+ - Run `black src/ tests/` before committing; CI uses `--check`.
150
+
151
+ ## Develop
152
+
153
+ ```bash
154
+ python -m venv .venv/dev
155
+ .venv/dev/Scripts/pip install -e ".[dev]" # POSIX: .venv/dev/bin/pip
156
+ .venv/dev/Scripts/pytest -q # POSIX: .venv/dev/bin/pytest -q
157
+ ```
158
+
159
+ Tests live in `tests/` (`test_ip.py`, `test_mac.py`, `test_net.py`,
160
+ `test_interfaces.py`) and run via `pytest -q` from a checkout;
161
+ `pyproject.toml` puts `src/` on the path. Run against **3.9 and 3.14** — 3.9 is
162
+ the floor, so no unquoted `X | Y` unions at runtime.
163
+
164
+ Code is formatted with **black** (`target-version = py39`, configured in
165
+ `pyproject.toml`; installed by the `dev` extra):
166
+
167
+ ```bash
168
+ .venv/dev/Scripts/black src/ tests/ # format
169
+ .venv/dev/Scripts/black --check src/ tests/ # verify, for CI
170
+ ```
171
+
172
+ ### Releasing
173
+
174
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
175
+ [`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the
176
+ release workflow: test gate → build → publish (PyPI) → docs deploy. Package
177
+ builds locally with `hatchling`.
178
+
179
+ ## License
180
+
181
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,70 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.0.0] - 2026-07-22
11
+
12
+ Initial release.
13
+
14
+ Earlier version numbers appear in this project's git history but were never
15
+ tagged or published, so there is no upgrade path to describe -- everything
16
+ below is simply what the package contains.
17
+
18
+ ### Added
19
+
20
+ - **Interface discovery** -- `get_interfaces()` reports adapter names, MACs,
21
+ MTU and *real* prefix lengths on Linux, macOS/BSD and Windows, via `ctypes`
22
+ bindings to `getifaddrs(3)` and `GetAdaptersAddresses`. No third-party
23
+ dependency. `Interface.is_loopback` is derived from the addresses rather than
24
+ the name, since `lo`, `lo0` and `Loopback Pseudo-Interface 1` share no
25
+ spelling. `Interface.primary_ip()` picks one entry; `iter_addresses()` is the
26
+ flattened per-address view.
27
+ - **Types and parsing** -- `IPAddress`/`IPInterface`/`IPNetwork` union aliases
28
+ to annotate with, and one `parse(value, type, **kwargs)` entry point with
29
+ non-raising `try_parse` and boolean `is_valid` siblings. Concrete types are
30
+ strict about family; networks are non-strict about host bits by default.
31
+ - **`MACAddress`** -- colon/hyphen/dot/bare plus `int`/`bytes`, hashable and
32
+ ordered, with `.packed`, `.oui`, `.is_multicast`, `.is_local` and
33
+ case-selectable rendering. A value type exposing `.packed`, not a `bytes`
34
+ subclass, matching how `ipaddress` models addresses.
35
+ - **Socket helpers** -- `bind()`, `bind_error_hint()`, `interface_for()`,
36
+ `get_source_ip()`, `get_free_port()`, `tcp_check()`, `wait_for_port()`.
37
+ - **`UdpEndpoint`** -- UDP receive reporting which interface a datagram arrived
38
+ on via `IP_PKTINFO`, degrading where `recvmsg` does not exist.
39
+ - **Routing and MTU** -- `get_route()` (first hop, unprivileged), `hop_count()`
40
+ (raw sockets or a traceroute fallback, so it works without elevation),
41
+ `discover_mtu()` (measures the real path -- `method="icmp"` with DF-flagged
42
+ pings, `"udp"` with datagrams, or `"tcp"` deriving from the negotiated MSS
43
+ since TCP cannot be probed), `get_pmtu()` (the kernel's cached answer, usually
44
+ `None`), `get_tcp_mss()`, and `Interface.mtu`. Header arithmetic is
45
+ family-aware: IPv6 adds 20 bytes over IPv4, and assuming v4 on a v6 path
46
+ under-reports by exactly that.
47
+ - **CIDR maths and host parsing** -- `collapse()`, `subtract()` (absent from
48
+ `ipaddress`), and `normalize_host()`, which keeps `"::1"` an address rather
49
+ than host `"::"` port `1`.
50
+ - **Scheme/port registry** -- `get_default_port()`, `get_default_scheme()`,
51
+ `register_port()`.
52
+ - **DNS** -- `resolve()` returning native types (`A`/`AAAA` as `ipaddress`
53
+ objects), `[]` on a genuine lookup failure, and `ValueError` for a malformed
54
+ query rather than a silent empty result.
55
+ - **`ping()`** -- returns a `PingResult` with round-trip time and TTL that stays
56
+ truthy. `method="icmp"|"tcp"|"udp"` reaches hosts through firewalls that drop
57
+ echo; all three ask "is the *host* up?", so a TCP refusal or an ICMP
58
+ port-unreachable counts as success. `tcp_check` remains the "is the *service*
59
+ up?" question, where a refusal is a failure. `ttl=` behaves identically on every platform, because Windows `ping`
60
+ exits 0 for "TTL expired in transit" and the reply address is verified
61
+ instead of the exit code.
62
+ - **Scanning** -- concurrent `scan_ports()` / `scan_hosts()`, ports addressable
63
+ by scheme name.
64
+ - **Multicast** -- `multicast_socket()`, `join_group()`, `leave_group()`,
65
+ wrapping a setup whose failure modes are otherwise silent.
66
+ - **`Host`**, **`retry()`/`backoff_delays()`**, and the named networks `APIPA`,
67
+ `LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6`.
68
+
69
+ [Unreleased]: https://github.com/jose-pr/netimps/compare/v0.0.0...HEAD
70
+ [0.0.0]: https://github.com/jose-pr/netimps/releases/tag/v0.0.0
netimps-0.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jose A.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
netimps-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: netimps
3
+ Version: 0.0.0
4
+ Summary: Small, self-contained network utilities: IP/MAC types, DNS lookup, ping, NIC discovery
5
+ Project-URL: Homepage, https://github.com/jose-pr/netimps
6
+ Project-URL: Documentation, https://jose-pr.github.io/netimps/
7
+ Project-URL: Issues, https://github.com/jose-pr/netimps/issues
8
+ Author: Jose A.
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Networking
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: dnspython
23
+ Provides-Extra: dev
24
+ Requires-Dist: black; extra == 'dev'
25
+ Requires-Dist: build; extra == 'dev'
26
+ Requires-Dist: hatchling; extra == 'dev'
27
+ Requires-Dist: pytest; extra == 'dev'
28
+ Requires-Dist: twine; extra == 'dev'
29
+ Provides-Extra: docs
30
+ Requires-Dist: mkdocs; extra == 'docs'
31
+ Requires-Dist: mkdocs-material; extra == 'docs'
32
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # netimps
36
+
37
+ [![Version](https://img.shields.io/pypi/v/netimps.svg)](https://pypi.org/project/netimps/)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/netimps.svg)](https://pypi.org/project/netimps/)
39
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
40
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/netimps/test.yml)](https://github.com/jose-pr/netimps/actions/workflows/test.yml)
41
+
42
+ **The network utilities every tool ends up rewriting** — interface discovery,
43
+ "which is my IP", reachability checks, CIDR maths, host:port parsing, DNS,
44
+ ping, scanning and multicast — as one typed, flat-import library.
45
+
46
+ Built on the standard library: the only runtime dependency is `dnspython`, and
47
+ only `resolve()` uses it. Interface enumeration, routing and multicast are
48
+ `ctypes` bindings to the platform's own APIs, so there is nothing to compile
49
+ and no wheel to miss for your platform.
50
+
51
+ ## Features
52
+
53
+ - **Interface discovery, no dependencies** — `get_interfaces()` gives adapter
54
+ names, MACs, MTU and **real prefix lengths** on Linux, macOS/BSD and Windows,
55
+ via `getifaddrs(3)` / `GetAdaptersAddresses`. No `ifaddr` required.
56
+ - **One parsing entry point** — `parse(value, type)` with non-raising
57
+ `try_parse` and boolean `is_valid` siblings, all typed so a checker narrows
58
+ the result.
59
+ - **`MACAddress`** — colon/hyphen/dot/bare plus `int`/`bytes`, hashable and
60
+ ordered, with `.oui`, `.is_multicast`, `.is_local` and case-selectable
61
+ rendering.
62
+ - **The socket helpers everyone rewrites** — `get_source_ip`, `get_free_port`,
63
+ `tcp_check`, `wait_for_port`.
64
+ - **Routing and MTU** — `get_route` (first hop, unprivileged), `hop_count`
65
+ (raw sockets *or* traceroute fallback), `discover_mtu` / `get_pmtu`, `Interface.mtu`.
66
+ - **CIDR set maths** — `collapse` and `subtract`, the latter missing from
67
+ `ipaddress` entirely.
68
+ - **`normalize_host`** — `host:port` splitting that gets IPv6 brackets right.
69
+ - **Scanning** — concurrent `scan_ports` / `scan_hosts`.
70
+ - **Socket setup** — `bind()` with the options named, `UdpEndpoint` for UDP
71
+ servers that need to know which interface a datagram arrived on, and
72
+ `retry()` for the backoff loop everyone writes without jitter.
73
+ - **Multicast** — `multicast_socket` handling the join dance whose failure
74
+ modes are otherwise silent.
75
+ - **DNS and ping** — `resolve()` returning native types; `ping()` returning
76
+ round-trip time and TTL, not just a boolean.
77
+
78
+ ## Installation
79
+
80
+ ```bash
81
+ pip install netimps
82
+ ```
83
+
84
+ Requires Python 3.9+.
85
+
86
+ ## Quick start
87
+
88
+ ```python
89
+ import netimps
90
+ from netimps import IPAddress, IPNetwork, MACAddress, parse
91
+
92
+ # Interfaces: names, MACs, MTU and real prefixes on every OS
93
+ for iface in netimps.get_interfaces():
94
+ print(iface.name, iface.mac, iface.mtu, [str(ip) for ip in iface.ips])
95
+ # 'Wi-Fi' 00:00:5e:00:53:01 1500 ['192.0.2.10/24', 'fe80::.../64']
96
+
97
+ # Types annotate; parse builds
98
+ def route(dst: IPAddress, via: IPNetwork) -> None: ...
99
+
100
+ parse("10.0.0.5") # IPv4Address('10.0.0.5')
101
+ parse("10.0.0.5/24", IPNetwork) # IPv4Network('10.0.0.0/24')
102
+ netimps.try_parse("nope", IPAddress) # None
103
+ netimps.is_valid("::1", IPAddress) # True
104
+
105
+ # Which of my addresses actually reaches that host?
106
+ netimps.get_source_ip("8.8.8.8") # IPv4Address('192.0.2.10')
107
+ netimps.get_route("8.8.8.8").gateway # IPv4Address('192.0.2.1')
108
+
109
+ # Honest reachability, and waiting for a service
110
+ netimps.tcp_check("example.com", 443) # True
111
+ netimps.wait_for_port("localhost", 5432, timeout=60)
112
+
113
+ # CIDR set maths
114
+ netimps.subtract(["10.0.0.0/24"], ["10.0.0.64/26"])
115
+ # [IPv4Network('10.0.0.0/26'), IPv4Network('10.0.0.128/25')]
116
+
117
+ # host:port, including the IPv6 case people get wrong
118
+ netimps.normalize_host("[::1]:8080") # ('::1', 8080)
119
+ netimps.normalize_host("::1") # ('::1', None) -- not port 1
120
+
121
+ # MAC addresses
122
+ mac = MACAddress("AA-BB-CC-DD-EE-FF")
123
+ mac.as_str("-", upper=True) # 'AA-BB-CC-DD-EE-FF'
124
+ mac.is_local, mac.oui.hex() # (True, 'aabbcc') -- AA has the U/L bit
125
+
126
+ # DNS returns native types
127
+ netimps.resolve("example.com")[0].is_global # an IPv4Address, not a str
128
+ netimps.resolve("example.com", "txt") # ['v=spf1 -all'] -- unquoted
129
+
130
+ # ping carries the details, and can use TCP or UDP where ICMP is blocked
131
+ result = netimps.ping("8.8.8.8")
132
+ result.ok, result.rtt_ms, result.ttl # (True, 9.0, 119)
133
+ netimps.ping("8.8.8.8", method="tcp", port=53) # times the handshake
134
+
135
+ # Path MTU, measured rather than guessed
136
+ netimps.discover_mtu("8.8.8.8") # 1500
137
+ netimps.discover_mtu("10.0.0.5", method="udp", port=9999)
138
+ netimps.get_tcp_mss("example.com", 443) # 1460
139
+
140
+ # Scanning and multicast
141
+ netimps.scan_ports("192.168.1.1", ["ssh", "https"]) # [22, 443]
142
+ sock = netimps.multicast_socket("224.0.0.251", 5353) # mDNS listener
143
+
144
+ # Server-side: bind with the options named, and know where packets came from
145
+ server = netimps.bind("", 6767, broadcast=True)
146
+ endpoint = netimps.UdpEndpoint(server)
147
+
148
+ # Retry with backoff and jitter
149
+ netimps.retry(lambda: netimps.tcp_check("example.com", 443), attempts=3)
150
+ ```
151
+
152
+ ## API overview
153
+
154
+ | Name | Purpose |
155
+ | --- | --- |
156
+ | `IPAddress`, `IPInterface`, `IPNetwork` | v4/v6 **union aliases** for annotations |
157
+ | `IPAddressLike`, `IPNetworkLike`, `MACLike` | accepted-input unions |
158
+ | `IPv4Address`, `IPv4Interface`, ... | stdlib concrete-type re-exports |
159
+ | `parse`, `try_parse`, `is_valid` | build a type from a value (raising / `None` / `bool`) |
160
+ | `MACAddress` | parse / classify / render MAC addresses |
161
+ | `get_interfaces`, `Interface`, `iter_addresses` | native cross-platform NIC discovery |
162
+ | `get_ip`, `is_link_scoped` | address resolution and scope classification |
163
+ | `collapse`, `subtract` | CIDR set maths |
164
+ | `normalize_host` | `host:port` splitting, IPv6-aware |
165
+ | `get_default_port`, `get_default_scheme`, `register_port` | scheme ↔ port registry |
166
+ | `resolve` | DNS lookup → native records (`[]` on failure) |
167
+ | `ping`, `PingResult` | reachability with RTT and TTL |
168
+ | `bind`, `bind_error_hint`, `interface_for` | socket creation and diagnosis |
169
+ | `get_source_ip`, `get_free_port`, `tcp_check`, `wait_for_port` | socket helpers |
170
+ | `UdpEndpoint`, `Datagram` | UDP receive with arrival interface (`IP_PKTINFO`) |
171
+ | `Host` | hostname-or-address value type |
172
+ | `retry`, `backoff_delays` | bounded retry with exponential backoff |
173
+ | `APIPA`, `LOOPBACK_V4`, `LOOPBACK_V6`, `LINK_LOCAL_V6` | named networks |
174
+ | `get_route`, `Route`, `hop_count` | routing and distance |
175
+ | `discover_mtu`, `get_pmtu`, `get_tcp_mss` | path MTU by ICMP/UDP/TCP, the kernel's cached guess, or the negotiated MSS |
176
+ | `scan_ports`, `scan_hosts`, `PORT_RANGES` | concurrent scanning |
177
+ | `multicast_socket`, `join_group`, `leave_group`, `is_multicast` | multicast |
178
+ | `HOST_DN` | `platform.node()`, captured at import time |
179
+
180
+ Full per-export reference, with contracts and gotchas, lives in
181
+ [`src/netimps/AGENTS.md`](src/netimps/AGENTS.md).
182
+
183
+ ## Design notes
184
+
185
+ A few behaviours are deliberate and worth knowing:
186
+
187
+ - **`Interface.is_loopback` is computed from addresses, not names** — `lo`,
188
+ `lo0` and `Loopback Pseudo-Interface 1` share no spelling.
189
+ - **Concrete types are strict about family.** `parse("::1", IPAddress)` works;
190
+ `parse("::1", IPv4Address)` raises rather than quietly returning v6.
191
+ - **Networks parse non-strict by default**, so `10.0.0.5/24` normalises instead
192
+ of raising. Pass `strict=True` for stdlib behaviour.
193
+ - **`resolve` raises on a malformed query** rather than returning `[]` — a
194
+ typo'd record type should not look like "no such record".
195
+ - **`ping(ttl=...)` behaves the same on every OS.** Windows `ping` exits `0`
196
+ for "TTL expired in transit", so the reply address is verified instead of
197
+ trusting the exit code.
198
+ - **`hop_count` works unprivileged**, falling back to the system traceroute
199
+ when a raw socket is unavailable.
200
+ - **`discover_mtu` measures; `get_pmtu` only reports what the kernel cached.**
201
+ The latter is usually `None`, and always `None` on Windows. On one real host
202
+ the local link was 9000 and the true path MTU 1500 — only probing found it.
203
+
204
+ ## Development
205
+
206
+ ```bash
207
+ python -m venv .venv/dev
208
+ .venv/dev/Scripts/pip install -e ".[dev]" # POSIX: .venv/dev/bin/pip
209
+ .venv/dev/Scripts/pytest -q
210
+ .venv/dev/Scripts/black src/ tests/
211
+ ```
212
+
213
+ Tested on Python 3.9 (the floor) and 3.14.
214
+
215
+ ### Releasing
216
+
217
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
218
+ [`CHANGELOG.md`](CHANGELOG.md). Pushing a tag matching `v*` triggers the release
219
+ workflow: test gate → build → publish → docs deploy.
220
+
221
+ ## License
222
+
223
+ MIT — see [LICENSE](LICENSE).