shadow-tools 0.2.0__tar.gz → 0.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: shadow-tools
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: A practical all-in-one Python toolkit for HTTP, WebSockets, files, data, validation, async work, and CLIs.
5
5
  Project-URL: Homepage, https://github.com/WilliamLaurento/shadow-tools
6
6
  Project-URL: Issues, https://github.com/WilliamLaurento/shadow-tools/issues
@@ -59,11 +59,13 @@ assert is_email("person@example.com")
59
59
  ## DNS, subdomains, and test IPs
60
60
 
61
61
  ```python
62
- from shadowtools import generate_test_ips, make_subdomain, resolve_dns
62
+ from shadowtools import generate_random_ips, generate_test_ips, make_subdomain, resolve_dns
63
63
 
64
64
  print(make_subdomain("api", "example.com")) # api.example.com
65
65
  print(resolve_dns("example.com"))
66
66
  print(generate_test_ips(2)) # safe documentation-only addresses
67
+ print(generate_random_ips(5)) # mixed private, public, and special IPv4 addresses
68
+ print(generate_random_ips(3, family=6))
67
69
  ```
68
70
 
69
71
  ## AI app helpers
@@ -35,11 +35,13 @@ assert is_email("person@example.com")
35
35
  ## DNS, subdomains, and test IPs
36
36
 
37
37
  ```python
38
- from shadowtools import generate_test_ips, make_subdomain, resolve_dns
38
+ from shadowtools import generate_random_ips, generate_test_ips, make_subdomain, resolve_dns
39
39
 
40
40
  print(make_subdomain("api", "example.com")) # api.example.com
41
41
  print(resolve_dns("example.com"))
42
42
  print(generate_test_ips(2)) # safe documentation-only addresses
43
+ print(generate_random_ips(5)) # mixed private, public, and special IPv4 addresses
44
+ print(generate_random_ips(3, family=6))
43
45
  ```
44
46
 
45
47
  ## AI app helpers
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "shadow-tools"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "A practical all-in-one Python toolkit for HTTP, WebSockets, files, data, validation, async work, and CLIs."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -3,13 +3,13 @@
3
3
  from .data import read_csv, read_json, write_csv, write_json
4
4
  from .files import append_text, ensure_dir, read_text, write_text
5
5
  from .http import get, request
6
- from .network import generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns
6
+ from .network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns
7
7
  from .validation import is_email, is_url, require_keys
8
8
 
9
- __version__ = "0.2.0"
9
+ __version__ = "0.2.1"
10
10
 
11
11
  __all__ = [
12
- "append_text", "ensure_dir", "generate_test_ips", "get", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "read_csv",
12
+ "append_text", "ensure_dir", "generate_random_ips", "generate_test_ips", "get", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "read_csv",
13
13
  "read_json", "read_text", "request", "require_keys", "write_csv",
14
14
  "write_json", "write_text", "resolve_dns",
15
15
  ]
@@ -0,0 +1,94 @@
1
+ """Small network helpers for DNS lookups and safe test data."""
2
+ from __future__ import annotations
3
+
4
+ import ipaddress
5
+ import random
6
+ import socket
7
+
8
+
9
+ def resolve_dns(hostname: str) -> dict[str, list[str]]:
10
+ """Resolve a hostname into deduplicated IPv4 and IPv6 addresses."""
11
+ results: dict[str, set[str]] = {"ipv4": set(), "ipv6": set()}
12
+ for family, _, _, _, address in socket.getaddrinfo(hostname, None):
13
+ if family == socket.AF_INET:
14
+ results["ipv4"].add(address[0])
15
+ elif family == socket.AF_INET6:
16
+ results["ipv6"].add(address[0])
17
+ return {kind: sorted(addresses) for kind, addresses in results.items()}
18
+
19
+
20
+ def is_subdomain_of(hostname: str, domain: str) -> bool:
21
+ """Return whether hostname is the domain itself or one of its subdomains."""
22
+ cleaned_host = hostname.lower().rstrip(".")
23
+ cleaned_domain = domain.lower().lstrip(".").rstrip(".")
24
+ return cleaned_host == cleaned_domain or cleaned_host.endswith("." + cleaned_domain)
25
+
26
+
27
+ def make_subdomain(label: str, domain: str) -> str:
28
+ """Build a normalized subdomain name, such as api.example.com."""
29
+ label = label.strip().lower().strip(".")
30
+ domain = domain.strip().lower().strip(".")
31
+ if not label or not domain:
32
+ raise ValueError("label and domain are required")
33
+ return f"{label}.{domain}"
34
+
35
+
36
+ def generate_test_ips(count: int = 1, *, family: int = 4) -> list[str]:
37
+ """Generate documentation-only IP addresses safe for examples and tests."""
38
+ if not 1 <= count <= 10_000:
39
+ raise ValueError("count must be between 1 and 10,000")
40
+ network = ipaddress.ip_network("2001:db8::/64" if family == 6 else "192.0.2.0/24")
41
+ return [str(network.network_address + random.randint(1, network.num_addresses - 2)) for _ in range(count)]
42
+
43
+
44
+ def generate_random_ips(count: int = 1, *, family: int = 4,
45
+ include_special: bool = True) -> list[str]:
46
+ """Generate random IPv4 or IPv6 addresses for test data and mock inputs.
47
+
48
+ When ``include_special`` is true, the results can include private, public,
49
+ loopback, link-local, unspecified, and (for IPv4) broadcast addresses.
50
+ This function only creates address strings; it never connects to them.
51
+ """
52
+ if not 1 <= count <= 10_000:
53
+ raise ValueError("count must be between 1 and 10,000")
54
+ if family not in (4, 6):
55
+ raise ValueError("family must be 4 or 6")
56
+ kinds = ["private", "public"]
57
+ if include_special:
58
+ kinds += ["loopback", "link_local", "unspecified"]
59
+ if family == 4:
60
+ kinds.append("broadcast")
61
+ return [_random_ip(random.choice(kinds), family) for _ in range(count)]
62
+
63
+
64
+ def _random_ip(kind: str, family: int) -> str:
65
+ if family == 4:
66
+ if kind == "private":
67
+ network = random.choice([ipaddress.ip_network("10.0.0.0/8"),
68
+ ipaddress.ip_network("172.16.0.0/12"),
69
+ ipaddress.ip_network("192.168.0.0/16")])
70
+ return str(network.network_address + random.randint(1, network.num_addresses - 2))
71
+ if kind == "loopback":
72
+ return str(ipaddress.IPv4Address((127 << 24) | random.randint(1, (1 << 24) - 2)))
73
+ if kind == "link_local":
74
+ return str(ipaddress.IPv4Address((169 << 24) | (254 << 16) | random.randint(1, (1 << 16) - 2)))
75
+ if kind == "unspecified":
76
+ return "0.0.0.0"
77
+ if kind == "broadcast":
78
+ return "255.255.255.255"
79
+ while True:
80
+ candidate = ipaddress.IPv4Address(random.getrandbits(32))
81
+ if candidate.is_global:
82
+ return str(candidate)
83
+ if kind == "private":
84
+ return str(ipaddress.IPv6Address((0xFD << 120) | random.getrandbits(120)))
85
+ if kind == "loopback":
86
+ return "::1"
87
+ if kind == "link_local":
88
+ return str(ipaddress.IPv6Address((0xFE80 << 112) | random.getrandbits(54)))
89
+ if kind == "unspecified":
90
+ return "::"
91
+ while True:
92
+ candidate = ipaddress.IPv6Address(random.getrandbits(128))
93
+ if candidate.is_global:
94
+ return str(candidate)
@@ -5,7 +5,7 @@ import pytest
5
5
  from shadowtools.async_tools import run_all
6
6
  from shadowtools.data import read_csv, read_json, write_csv, write_json
7
7
  from shadowtools.files import append_text, read_text, write_text
8
- from shadowtools.network import generate_test_ips, is_subdomain_of, make_subdomain
8
+ from shadowtools.network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain
9
9
  from shadowtools.ai import prompt, tool_schema
10
10
  from shadowtools.validation import is_email, is_url, require_keys
11
11
 
@@ -41,5 +41,7 @@ def test_network_and_ai_helpers():
41
41
  assert make_subdomain("API", "Example.COM") == "api.example.com"
42
42
  assert is_subdomain_of("api.example.com", "example.com")
43
43
  assert generate_test_ips(1)[0].startswith("192.0.2.")
44
+ assert len(generate_random_ips(3)) == 3
45
+ assert len(generate_random_ips(2, family=6)) == 2
44
46
  assert prompt("Hi {name}", name="Will") == "Hi Will"
45
47
  assert tool_schema("lookup_dns", "Lookup DNS", {"host": {"type": "string"}})["name"] == "lookup_dns"
@@ -1,41 +0,0 @@
1
- """Small network helpers for DNS lookups and safe test data."""
2
- from __future__ import annotations
3
-
4
- import ipaddress
5
- import random
6
- import socket
7
-
8
-
9
- def resolve_dns(hostname: str) -> dict[str, list[str]]:
10
- """Resolve a hostname into deduplicated IPv4 and IPv6 addresses."""
11
- results: dict[str, set[str]] = {"ipv4": set(), "ipv6": set()}
12
- for family, _, _, _, address in socket.getaddrinfo(hostname, None):
13
- if family == socket.AF_INET:
14
- results["ipv4"].add(address[0])
15
- elif family == socket.AF_INET6:
16
- results["ipv6"].add(address[0])
17
- return {kind: sorted(addresses) for kind, addresses in results.items()}
18
-
19
-
20
- def is_subdomain_of(hostname: str, domain: str) -> bool:
21
- """Return whether hostname is the domain itself or one of its subdomains."""
22
- cleaned_host = hostname.lower().rstrip(".")
23
- cleaned_domain = domain.lower().lstrip(".").rstrip(".")
24
- return cleaned_host == cleaned_domain or cleaned_host.endswith("." + cleaned_domain)
25
-
26
-
27
- def make_subdomain(label: str, domain: str) -> str:
28
- """Build a normalized subdomain name, such as api.example.com."""
29
- label = label.strip().lower().strip(".")
30
- domain = domain.strip().lower().strip(".")
31
- if not label or not domain:
32
- raise ValueError("label and domain are required")
33
- return f"{label}.{domain}"
34
-
35
-
36
- def generate_test_ips(count: int = 1, *, family: int = 4) -> list[str]:
37
- """Generate documentation-only IP addresses safe for examples and tests."""
38
- if not 1 <= count <= 10_000:
39
- raise ValueError("count must be between 1 and 10,000")
40
- network = ipaddress.ip_network("2001:db8::/64" if family == 6 else "192.0.2.0/24")
41
- return [str(network.network_address + random.randint(1, network.num_addresses - 2)) for _ in range(count)]
File without changes
File without changes