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.
- netimps-0.0.0/.gitignore +38 -0
- netimps-0.0.0/AGENTS.md +181 -0
- netimps-0.0.0/CHANGELOG.md +70 -0
- netimps-0.0.0/LICENSE +21 -0
- netimps-0.0.0/PKG-INFO +223 -0
- netimps-0.0.0/README.md +189 -0
- netimps-0.0.0/docs/api/reference.md +14 -0
- netimps-0.0.0/docs/changelog.md +3 -0
- netimps-0.0.0/docs/index.md +61 -0
- netimps-0.0.0/mkdocs.yml +51 -0
- netimps-0.0.0/pyproject.toml +57 -0
- netimps-0.0.0/src/netimps/AGENTS.md +424 -0
- netimps-0.0.0/src/netimps/__init__.py +405 -0
- netimps-0.0.0/src/netimps/_dns.py +160 -0
- netimps-0.0.0/src/netimps/_iface_spec.py +96 -0
- netimps-0.0.0/src/netimps/_ifaddrs.py +777 -0
- netimps-0.0.0/src/netimps/_ip.py +385 -0
- netimps-0.0.0/src/netimps/_mac.py +205 -0
- netimps-0.0.0/src/netimps/_multicast.py +197 -0
- netimps-0.0.0/src/netimps/_ping.py +399 -0
- netimps-0.0.0/src/netimps/_retry.py +130 -0
- netimps-0.0.0/src/netimps/_scan.py +245 -0
- netimps-0.0.0/src/netimps/_scheme.py +140 -0
- netimps-0.0.0/src/netimps/_sockets.py +1022 -0
- netimps-0.0.0/src/netimps/_udp.py +189 -0
- netimps-0.0.0/src/netimps/py.typed +0 -0
- netimps-0.0.0/tests/test_centralized.py +553 -0
- netimps-0.0.0/tests/test_interfaces.py +310 -0
- netimps-0.0.0/tests/test_ip.py +368 -0
- netimps-0.0.0/tests/test_mac.py +177 -0
- netimps-0.0.0/tests/test_net.py +479 -0
- netimps-0.0.0/tests/test_scan.py +274 -0
- netimps-0.0.0/tests/test_sockets.py +542 -0
netimps-0.0.0/.gitignore
ADDED
|
@@ -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/
|
netimps-0.0.0/AGENTS.md
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/netimps/)
|
|
38
|
+
[](https://pypi.org/project/netimps/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
[](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).
|