ip-reporter 0.1.1__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,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .DS_Store
10
+ .env
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IP Reporter contributors
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.
@@ -0,0 +1,231 @@
1
+ Metadata-Version: 2.4
2
+ Name: ip-reporter
3
+ Version: 0.1.1
4
+ Summary: Reliable WAN/LAN IP discovery and Cloudflare DNS synchronization
5
+ Author: IP Reporter contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 IP Reporter contributors
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: cloudflare,dynamic-dns,http3,ip,ipv4,ipv6
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Intended Audience :: Developers
31
+ Classifier: License :: OSI Approved :: MIT License
32
+ Classifier: Operating System :: MacOS
33
+ Classifier: Operating System :: POSIX
34
+ Classifier: Programming Language :: Python :: 3
35
+ Classifier: Programming Language :: Python :: 3 :: Only
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: System :: Networking
41
+ Requires-Python: >=3.10
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8.0; extra == 'dev'
44
+ Requires-Dist: ruff>=0.8; extra == 'dev'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # ip-reporter
48
+
49
+ `ip-reporter` is a dependency-free Python package and CLI for discovering local
50
+ LAN addresses, public WAN addresses, and public addresses obtained through
51
+ HTTP/2 or HTTP/3. It can synchronize one or many A/AAAA records in Cloudflare.
52
+
53
+ The import name is `ip_reporter`; the installed command is `ip-reporter`.
54
+
55
+ ## Install with uv
56
+
57
+ Install the command as a uv tool:
58
+
59
+ ```bash
60
+ uv tool install ip-reporter
61
+ ip-reporter --scope wan --family ipv4
62
+ ```
63
+
64
+ Use it as a library dependency:
65
+
66
+ ```bash
67
+ uv add ip-reporter
68
+ ```
69
+
70
+ For HTTP/2 and HTTP/3 probing on macOS, install Homebrew curl. The package
71
+ rejects `/usr/bin/curl` on macOS so the system curl cannot silently disable
72
+ those probes.
73
+
74
+ ```bash
75
+ brew install curl
76
+ export IP_REPORTER_CURL="$(brew --prefix curl)/bin/curl"
77
+ ```
78
+
79
+ The selected curl must report `HTTP2` or `HTTP3` in `curl --version` depending
80
+ on the requested probe. HTTP/3 requires a curl build with HTTP/3 support.
81
+
82
+ ## CLI
83
+
84
+ With no subcommand, the CLI performs discovery. The default family is IPv4.
85
+
86
+ ```bash
87
+ # Public IPv4; text output includes the scope, family, and transport.
88
+ ip-reporter --scope wan --family ipv4
89
+
90
+ # LAN IPv4 and IPv6.
91
+ ip-reporter discover --scope lan --family ipv4 --family ipv6
92
+
93
+ # Force HTTP/3 over IPv4 and IPv6.
94
+ ip-reporter --scope wan --family h3ipv4 --family h3ipv6
95
+
96
+ # Structured output for scripts.
97
+ ip-reporter --scope both --family ipv4 --family ipv6 --format json
98
+
99
+ # Print only address values, one per line.
100
+ ip-reporter --scope wan --family ipv4 --format values
101
+ ```
102
+
103
+ WAN probing has independent failover endpoints. The defaults include two
104
+ providers for IPv4, two for IPv6, and two candidates for each HTTP/3 family.
105
+ Override them with repeated `--wan-endpoint` options or with the Python
106
+ `RemoteEndpoint` API.
107
+
108
+ ## Cloudflare DNS
109
+
110
+ Use an API token with the DNS Write permission. Prefer environment variables so
111
+ credentials do not enter shell history:
112
+
113
+ ```bash
114
+ export CLOUDFLARE_API_TOKEN="..."
115
+ export CLOUDFLARE_ZONE_ID="..."
116
+ ```
117
+
118
+ Update one existing record:
119
+
120
+ ```bash
121
+ ip-reporter dns update \
122
+ --record home.example.com \
123
+ --family ipv4
124
+ ```
125
+
126
+ Update many names and both address families:
127
+
128
+ ```bash
129
+ ip-reporter dns update \
130
+ --record home.example.com \
131
+ --record vpn.example.com \
132
+ --family ipv4 \
133
+ --family ipv6 \
134
+ --ttl 60 \
135
+ --not-proxied
136
+ ```
137
+
138
+ Existing records are updated with PATCH and unchanged values are skipped.
139
+ Missing records are not created unless `--create-missing` is supplied. Stale
140
+ records are not deleted. Use `--dry-run` to inspect discovery without changing
141
+ Cloudflare.
142
+
143
+ If you have a zone name rather than an ID, pass `--zone-name`; the client can
144
+ resolve it through the Cloudflare API. Relative names and `@` are accepted when
145
+ the zone name is supplied:
146
+
147
+ ```bash
148
+ ip-reporter dns update \
149
+ --zone-name example.com \
150
+ --record home \
151
+ --family ipv4 \
152
+ --create-missing
153
+ ```
154
+
155
+ `h3ipv4` writes an A record and `h3ipv6` writes an AAAA record. The HTTP/3
156
+ transport is discovery metadata; the DNS type is determined from the resulting
157
+ IP address.
158
+
159
+ ## Python API
160
+
161
+ The structured API is suitable for another package, a scheduler, or a daemon:
162
+
163
+ ```python
164
+ from ip_reporter import AddressFamily, IPReporter
165
+
166
+ reporter = IPReporter()
167
+ report = reporter.discover(
168
+ scope="wan",
169
+ families=(AddressFamily.IPV4, AddressFamily.IPV6),
170
+ )
171
+
172
+ for address in report.wan:
173
+ print(address.value, address.family.value, address.protocol.value)
174
+
175
+ # Convenience methods return only strings.
176
+ wan_ipv4 = reporter.get_wan(families=(AddressFamily.IPV4,))
177
+ lan_ipv6 = reporter.get_lan(families=(AddressFamily.IPV6,))
178
+ ```
179
+
180
+ The discovery dependencies are injectable, which makes applications and tests
181
+ independent of the network:
182
+
183
+ ```python
184
+ from ip_reporter import AddressDiscovery, AddressFamily, IPReporter, RemoteEndpoint
185
+
186
+ class Probe:
187
+ def fetch_ip(self, url, family):
188
+ return ("203.0.113.10", "http2")
189
+
190
+ reporter = IPReporter(
191
+ AddressDiscovery(
192
+ wan_probe=Probe(),
193
+ endpoints=(RemoteEndpoint("https://probe.example/ip", (AddressFamily.IPV4,)),),
194
+ )
195
+ )
196
+ ```
197
+
198
+ Synchronize Cloudflare from the same service:
199
+
200
+ ```python
201
+ from ip_reporter import AddressFamily, IPReporter
202
+
203
+ updates = IPReporter().update_cloudflare_dns(
204
+ ["home.example.com", "vpn.example.com"],
205
+ zone_id="your-zone-id",
206
+ families=(AddressFamily.IPV4, AddressFamily.IPV6),
207
+ create_missing=True,
208
+ )
209
+ print(updates.as_dict())
210
+ ```
211
+
212
+ `CloudflareClient.from_env()` uses `CLOUDFLARE_API_TOKEN`, or the legacy
213
+ `CLOUDFLARE_EMAIL` and `CLOUDFLARE_API_KEY` pair. A custom HTTP transport can
214
+ be injected into `CloudflareClient` for testing.
215
+
216
+ ## Build and publish
217
+
218
+ From a checkout:
219
+
220
+ ```bash
221
+ uv sync --extra dev
222
+ uv run pytest
223
+ uv build
224
+ uv publish
225
+ ```
226
+
227
+ Set `UV_PUBLISH_TOKEN` before `uv publish`, or configure another uv-supported
228
+ PyPI credential method. Bump `project.version` in `pyproject.toml` for each
229
+ release.
230
+
231
+ Cloudflare API details: [DNS record create](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/), [DNS record update](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/).
@@ -0,0 +1,185 @@
1
+ # ip-reporter
2
+
3
+ `ip-reporter` is a dependency-free Python package and CLI for discovering local
4
+ LAN addresses, public WAN addresses, and public addresses obtained through
5
+ HTTP/2 or HTTP/3. It can synchronize one or many A/AAAA records in Cloudflare.
6
+
7
+ The import name is `ip_reporter`; the installed command is `ip-reporter`.
8
+
9
+ ## Install with uv
10
+
11
+ Install the command as a uv tool:
12
+
13
+ ```bash
14
+ uv tool install ip-reporter
15
+ ip-reporter --scope wan --family ipv4
16
+ ```
17
+
18
+ Use it as a library dependency:
19
+
20
+ ```bash
21
+ uv add ip-reporter
22
+ ```
23
+
24
+ For HTTP/2 and HTTP/3 probing on macOS, install Homebrew curl. The package
25
+ rejects `/usr/bin/curl` on macOS so the system curl cannot silently disable
26
+ those probes.
27
+
28
+ ```bash
29
+ brew install curl
30
+ export IP_REPORTER_CURL="$(brew --prefix curl)/bin/curl"
31
+ ```
32
+
33
+ The selected curl must report `HTTP2` or `HTTP3` in `curl --version` depending
34
+ on the requested probe. HTTP/3 requires a curl build with HTTP/3 support.
35
+
36
+ ## CLI
37
+
38
+ With no subcommand, the CLI performs discovery. The default family is IPv4.
39
+
40
+ ```bash
41
+ # Public IPv4; text output includes the scope, family, and transport.
42
+ ip-reporter --scope wan --family ipv4
43
+
44
+ # LAN IPv4 and IPv6.
45
+ ip-reporter discover --scope lan --family ipv4 --family ipv6
46
+
47
+ # Force HTTP/3 over IPv4 and IPv6.
48
+ ip-reporter --scope wan --family h3ipv4 --family h3ipv6
49
+
50
+ # Structured output for scripts.
51
+ ip-reporter --scope both --family ipv4 --family ipv6 --format json
52
+
53
+ # Print only address values, one per line.
54
+ ip-reporter --scope wan --family ipv4 --format values
55
+ ```
56
+
57
+ WAN probing has independent failover endpoints. The defaults include two
58
+ providers for IPv4, two for IPv6, and two candidates for each HTTP/3 family.
59
+ Override them with repeated `--wan-endpoint` options or with the Python
60
+ `RemoteEndpoint` API.
61
+
62
+ ## Cloudflare DNS
63
+
64
+ Use an API token with the DNS Write permission. Prefer environment variables so
65
+ credentials do not enter shell history:
66
+
67
+ ```bash
68
+ export CLOUDFLARE_API_TOKEN="..."
69
+ export CLOUDFLARE_ZONE_ID="..."
70
+ ```
71
+
72
+ Update one existing record:
73
+
74
+ ```bash
75
+ ip-reporter dns update \
76
+ --record home.example.com \
77
+ --family ipv4
78
+ ```
79
+
80
+ Update many names and both address families:
81
+
82
+ ```bash
83
+ ip-reporter dns update \
84
+ --record home.example.com \
85
+ --record vpn.example.com \
86
+ --family ipv4 \
87
+ --family ipv6 \
88
+ --ttl 60 \
89
+ --not-proxied
90
+ ```
91
+
92
+ Existing records are updated with PATCH and unchanged values are skipped.
93
+ Missing records are not created unless `--create-missing` is supplied. Stale
94
+ records are not deleted. Use `--dry-run` to inspect discovery without changing
95
+ Cloudflare.
96
+
97
+ If you have a zone name rather than an ID, pass `--zone-name`; the client can
98
+ resolve it through the Cloudflare API. Relative names and `@` are accepted when
99
+ the zone name is supplied:
100
+
101
+ ```bash
102
+ ip-reporter dns update \
103
+ --zone-name example.com \
104
+ --record home \
105
+ --family ipv4 \
106
+ --create-missing
107
+ ```
108
+
109
+ `h3ipv4` writes an A record and `h3ipv6` writes an AAAA record. The HTTP/3
110
+ transport is discovery metadata; the DNS type is determined from the resulting
111
+ IP address.
112
+
113
+ ## Python API
114
+
115
+ The structured API is suitable for another package, a scheduler, or a daemon:
116
+
117
+ ```python
118
+ from ip_reporter import AddressFamily, IPReporter
119
+
120
+ reporter = IPReporter()
121
+ report = reporter.discover(
122
+ scope="wan",
123
+ families=(AddressFamily.IPV4, AddressFamily.IPV6),
124
+ )
125
+
126
+ for address in report.wan:
127
+ print(address.value, address.family.value, address.protocol.value)
128
+
129
+ # Convenience methods return only strings.
130
+ wan_ipv4 = reporter.get_wan(families=(AddressFamily.IPV4,))
131
+ lan_ipv6 = reporter.get_lan(families=(AddressFamily.IPV6,))
132
+ ```
133
+
134
+ The discovery dependencies are injectable, which makes applications and tests
135
+ independent of the network:
136
+
137
+ ```python
138
+ from ip_reporter import AddressDiscovery, AddressFamily, IPReporter, RemoteEndpoint
139
+
140
+ class Probe:
141
+ def fetch_ip(self, url, family):
142
+ return ("203.0.113.10", "http2")
143
+
144
+ reporter = IPReporter(
145
+ AddressDiscovery(
146
+ wan_probe=Probe(),
147
+ endpoints=(RemoteEndpoint("https://probe.example/ip", (AddressFamily.IPV4,)),),
148
+ )
149
+ )
150
+ ```
151
+
152
+ Synchronize Cloudflare from the same service:
153
+
154
+ ```python
155
+ from ip_reporter import AddressFamily, IPReporter
156
+
157
+ updates = IPReporter().update_cloudflare_dns(
158
+ ["home.example.com", "vpn.example.com"],
159
+ zone_id="your-zone-id",
160
+ families=(AddressFamily.IPV4, AddressFamily.IPV6),
161
+ create_missing=True,
162
+ )
163
+ print(updates.as_dict())
164
+ ```
165
+
166
+ `CloudflareClient.from_env()` uses `CLOUDFLARE_API_TOKEN`, or the legacy
167
+ `CLOUDFLARE_EMAIL` and `CLOUDFLARE_API_KEY` pair. A custom HTTP transport can
168
+ be injected into `CloudflareClient` for testing.
169
+
170
+ ## Build and publish
171
+
172
+ From a checkout:
173
+
174
+ ```bash
175
+ uv sync --extra dev
176
+ uv run pytest
177
+ uv build
178
+ uv publish
179
+ ```
180
+
181
+ Set `UV_PUBLISH_TOKEN` before `uv publish`, or configure another uv-supported
182
+ PyPI credential method. Bump `project.version` in `pyproject.toml` for each
183
+ release.
184
+
185
+ Cloudflare API details: [DNS record create](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/create/), [DNS record update](https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/).
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.25"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ip-reporter"
7
+ version = "0.1.1"
8
+ description = "Reliable WAN/LAN IP discovery and Cloudflare DNS synchronization"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { file = "LICENSE" }
12
+ authors = [
13
+ { name = "IP Reporter contributors" },
14
+ ]
15
+ keywords = ["ip", "ipv4", "ipv6", "http3", "cloudflare", "dynamic-dns"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: MacOS",
21
+ "Operating System :: POSIX",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "Programming Language :: Python :: 3.12",
27
+ "Programming Language :: Python :: 3.13",
28
+ "Topic :: System :: Networking",
29
+ ]
30
+ dependencies = []
31
+
32
+ [project.optional-dependencies]
33
+ dev = [
34
+ "pytest>=8.0",
35
+ "ruff>=0.8",
36
+ ]
37
+
38
+ [project.scripts]
39
+ ip-reporter = "ip_reporter.cli:main"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/ip_reporter"]
43
+
44
+ [tool.pytest.ini_options]
45
+ addopts = "-q"
46
+ testpaths = ["tests"]
47
+
48
+ [tool.ruff]
49
+ line-length = 100
50
+ target-version = "py310"
51
+
52
+ [tool.ruff.lint]
53
+ select = ["E", "F", "I", "UP"]
@@ -0,0 +1,34 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Load environment variables from .env if it exists
5
+ if [ -f .env ]; then
6
+ echo "Loading environment variables from .env..."
7
+ # Export variables from .env, ignoring comments and empty lines
8
+ export $(grep -v '^#' .env | grep -v '^[[:space:]]*$' | xargs)
9
+ fi
10
+
11
+ # Map PYPI_TOKEN or PYPI to UV_PUBLISH_TOKEN if present
12
+ if [ -n "$PYPI_TOKEN" ] && [ -z "$UV_PUBLISH_TOKEN" ]; then
13
+ export UV_PUBLISH_TOKEN="$PYPI_TOKEN"
14
+ elif [ -n "$PYPI" ] && [ -z "$UV_PUBLISH_TOKEN" ]; then
15
+ export UV_PUBLISH_TOKEN="$PYPI"
16
+ fi
17
+
18
+ if [ -z "$UV_PUBLISH_TOKEN" ]; then
19
+ echo "Error: Neither UV_PUBLISH_TOKEN nor PYPI_TOKEN is set in .env or the current shell environment."
20
+ echo "Please create a .env file or export the variable. Example:"
21
+ echo "PYPI_TOKEN=pypi-..."
22
+ exit 1
23
+ fi
24
+
25
+ echo "Cleaning up dist/ directory..."
26
+ rm -rf dist/
27
+
28
+ echo "Building package..."
29
+ uv build
30
+
31
+ echo "Publishing package to PyPI..."
32
+ uv publish
33
+
34
+ echo "Successfully published to PyPI!"
@@ -0,0 +1,85 @@
1
+ """WAN/LAN IP discovery and Cloudflare DNS synchronization."""
2
+
3
+ from .cloudflare import (
4
+ CloudflareAPIError,
5
+ CloudflareClient,
6
+ CloudflareError,
7
+ HTTPResponse,
8
+ HTTPTransport,
9
+ RecordNotFoundError,
10
+ UrllibTransport,
11
+ )
12
+ from .curl import (
13
+ CurlClient,
14
+ CurlError,
15
+ CurlFeatureError,
16
+ CurlProbeError,
17
+ CurlUnavailableError,
18
+ CurlVersion,
19
+ find_curl,
20
+ )
21
+ from .discovery import (
22
+ DEFAULT_WAN_ENDPOINTS,
23
+ AddressDiscovery,
24
+ DiscoveryFailedError,
25
+ LocalAddressProvider,
26
+ RemoteEndpoint,
27
+ SocketLocalAddressProvider,
28
+ WanProbe,
29
+ )
30
+ from .models import (
31
+ AddressFamily,
32
+ DiscoveredAddress,
33
+ DiscoveryError,
34
+ DiscoveryResult,
35
+ DNSUpdate,
36
+ DNSUpdateResult,
37
+ ProbeProtocol,
38
+ Scope,
39
+ normalize_families,
40
+ )
41
+ from .reporter import (
42
+ IPReporter,
43
+ discover,
44
+ get_lan,
45
+ get_wan,
46
+ update_cloudflare_dns,
47
+ )
48
+
49
+ __all__ = [
50
+ "AddressDiscovery",
51
+ "AddressFamily",
52
+ "CloudflareAPIError",
53
+ "CloudflareClient",
54
+ "CloudflareError",
55
+ "CurlClient",
56
+ "CurlError",
57
+ "CurlFeatureError",
58
+ "CurlProbeError",
59
+ "CurlUnavailableError",
60
+ "CurlVersion",
61
+ "DEFAULT_WAN_ENDPOINTS",
62
+ "DNSUpdate",
63
+ "DNSUpdateResult",
64
+ "DiscoveryError",
65
+ "DiscoveryFailedError",
66
+ "DiscoveryResult",
67
+ "DiscoveredAddress",
68
+ "HTTPResponse",
69
+ "HTTPTransport",
70
+ "IPReporter",
71
+ "LocalAddressProvider",
72
+ "ProbeProtocol",
73
+ "RecordNotFoundError",
74
+ "RemoteEndpoint",
75
+ "Scope",
76
+ "SocketLocalAddressProvider",
77
+ "UrllibTransport",
78
+ "WanProbe",
79
+ "discover",
80
+ "find_curl",
81
+ "get_lan",
82
+ "get_wan",
83
+ "normalize_families",
84
+ "update_cloudflare_dns",
85
+ ]