shadow-tools 0.2.3__tar.gz → 0.2.5__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.
Files changed (26) hide show
  1. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/PKG-INFO +6 -4
  2. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/README.md +5 -3
  3. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/pyproject.toml +1 -1
  4. shadow_tools-0.2.5/release-0.2.3/shadow_tools-0.2.3-py3-none-any.whl +0 -0
  5. shadow_tools-0.2.5/release-0.2.3/shadow_tools-0.2.3.tar.gz +0 -0
  6. shadow_tools-0.2.5/release-0.2.4/shadow_tools-0.2.4-py3-none-any.whl +0 -0
  7. shadow_tools-0.2.5/release-0.2.4/shadow_tools-0.2.4.tar.gz +0 -0
  8. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/__init__.py +1 -1
  9. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/network.py +17 -10
  10. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/tests/test_core.py +5 -3
  11. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/.gitignore +0 -0
  12. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/LICENSE +0 -0
  13. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.0/shadow_tools-0.2.0-py3-none-any.whl +0 -0
  14. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.0/shadow_tools-0.2.0.tar.gz +0 -0
  15. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.1/shadow_tools-0.2.1-py3-none-any.whl +0 -0
  16. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.1/shadow_tools-0.2.1.tar.gz +0 -0
  17. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.2/shadow_tools-0.2.2-py3-none-any.whl +0 -0
  18. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/release-0.2.2/shadow_tools-0.2.2.tar.gz +0 -0
  19. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/ai.py +0 -0
  20. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/async_tools.py +0 -0
  21. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/cli.py +0 -0
  22. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/data.py +0 -0
  23. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/files.py +0 -0
  24. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/http.py +0 -0
  25. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/validation.py +0 -0
  26. {shadow_tools-0.2.3 → shadow_tools-0.2.5}/src/shadowtools/websocket.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: shadow-tools
3
- Version: 0.2.3
3
+ Version: 0.2.5
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
@@ -64,8 +64,9 @@ from shadowtools import generate_random_ips, generate_test_ips, make_subdomain,
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
67
+ print(generate_random_ips(5)) # globally routable public IPv4 addresses only
68
68
  print(generate_random_ips(3, family=6))
69
+ print(generate_random_ips(3, include_private=True, include_special=True))
69
70
  ```
70
71
 
71
72
  Reverse-DNS lookup for IP addresses:
@@ -73,11 +74,12 @@ Reverse-DNS lookup for IP addresses:
73
74
  ```python
74
75
  from shadowtools import generate_random_ips, reverse_dns
75
76
 
76
- for ip in generate_random_ips(5):
77
- print(reverse_dns(ip))
77
+ for result in reverse_dns(generate_random_ips(5)):
78
+ print(result) # only IPs with hostnames are shown
78
79
 
79
80
  # Or look up every generated IP at once.
80
81
  print(reverse_dns(generate_random_ips(5)))
82
+ print(reverse_dns(generate_random_ips(5), only_hostnames=False)) # include misses
81
83
  ```
82
84
 
83
85
  ## AI app helpers
@@ -40,8 +40,9 @@ from shadowtools import generate_random_ips, generate_test_ips, make_subdomain,
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
43
+ print(generate_random_ips(5)) # globally routable public IPv4 addresses only
44
44
  print(generate_random_ips(3, family=6))
45
+ print(generate_random_ips(3, include_private=True, include_special=True))
45
46
  ```
46
47
 
47
48
  Reverse-DNS lookup for IP addresses:
@@ -49,11 +50,12 @@ Reverse-DNS lookup for IP addresses:
49
50
  ```python
50
51
  from shadowtools import generate_random_ips, reverse_dns
51
52
 
52
- for ip in generate_random_ips(5):
53
- print(reverse_dns(ip))
53
+ for result in reverse_dns(generate_random_ips(5)):
54
+ print(result) # only IPs with hostnames are shown
54
55
 
55
56
  # Or look up every generated IP at once.
56
57
  print(reverse_dns(generate_random_ips(5)))
58
+ print(reverse_dns(generate_random_ips(5), only_hostnames=False)) # include misses
57
59
  ```
58
60
 
59
61
  ## 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.3"
7
+ version = "0.2.5"
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"
@@ -6,7 +6,7 @@ from .http import get, request
6
6
  from .network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns, reverse_dns
7
7
  from .validation import is_email, is_url, require_keys
8
8
 
9
- __version__ = "0.2.3"
9
+ __version__ = "0.2.5"
10
10
 
11
11
  __all__ = [
12
12
  "append_text", "ensure_dir", "generate_random_ips", "generate_test_ips", "get", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "read_csv",
@@ -18,20 +18,22 @@ def resolve_dns(hostname: str) -> dict[str, list[str]]:
18
18
  return {kind: sorted(addresses) for kind, addresses in results.items()}
19
19
 
20
20
 
21
- def reverse_dns(ip: str | Iterable[str]) -> dict[str, object] | list[dict[str, object]]:
21
+ def reverse_dns(ip: str | Iterable[str], *, only_hostnames: bool = True) -> dict[str, object] | list[dict[str, object]] | None:
22
22
  """Look up DNS names for one IP address or each address in a list.
23
23
 
24
- An address without a PTR record returns ``hostname: None`` instead of
25
- raising an exception.
24
+ List lookups return only results with a hostname by default. Set
25
+ ``only_hostnames=False`` to include addresses without PTR records.
26
26
  """
27
27
  if not isinstance(ip, str):
28
- return [reverse_dns(one_ip) for one_ip in ip]
28
+ results = [reverse_dns(one_ip, only_hostnames=False) for one_ip in ip]
29
+ return [result for result in results if not only_hostnames or result["hostname"]]
29
30
  address = ipaddress.ip_address(ip)
30
31
  try:
31
32
  hostname, aliases, _ = socket.gethostbyaddr(str(address))
32
33
  return {"ip": str(address), "hostname": hostname, "aliases": aliases}
33
34
  except (socket.herror, socket.gaierror):
34
- return {"ip": str(address), "hostname": None, "aliases": []}
35
+ result = {"ip": str(address), "hostname": None, "aliases": []}
36
+ return None if only_hostnames else result
35
37
 
36
38
 
37
39
  def is_subdomain_of(hostname: str, domain: str) -> bool:
@@ -59,18 +61,23 @@ def generate_test_ips(count: int = 1, *, family: int = 4) -> list[str]:
59
61
 
60
62
 
61
63
  def generate_random_ips(count: int = 1, *, family: int = 4,
62
- include_special: bool = True) -> list[str]:
64
+ include_private: bool = False,
65
+ include_special: bool = False) -> list[str]:
63
66
  """Generate random IPv4 or IPv6 addresses for test data and mock inputs.
64
67
 
65
- When ``include_special`` is true, the results can include private, public,
66
- loopback, link-local, unspecified, and (for IPv4) broadcast addresses.
67
- This function only creates address strings; it never connects to them.
68
+ By default, results are globally routable public addresses only. Set
69
+ ``include_private=True`` to include private ranges, or
70
+ ``include_special=True`` to include loopback, link-local, unspecified, and
71
+ broadcast-style addresses. This function only creates address strings; it
72
+ never connects to them.
68
73
  """
69
74
  if not 1 <= count <= 10_000:
70
75
  raise ValueError("count must be between 1 and 10,000")
71
76
  if family not in (4, 6):
72
77
  raise ValueError("family must be 4 or 6")
73
- kinds = ["private", "public"]
78
+ kinds = ["public"]
79
+ if include_private:
80
+ kinds.append("private")
74
81
  if include_special:
75
82
  kinds += ["loopback", "link_local", "unspecified"]
76
83
  if family == 4:
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import ipaddress
2
3
 
3
4
  import pytest
4
5
 
@@ -41,9 +42,10 @@ def test_network_and_ai_helpers():
41
42
  assert make_subdomain("API", "Example.COM") == "api.example.com"
42
43
  assert is_subdomain_of("api.example.com", "example.com")
43
44
  assert generate_test_ips(1)[0].startswith("192.0.2.")
44
- assert len(generate_random_ips(3)) == 3
45
+ assert all(ipaddress.ip_address(ip).is_global for ip in generate_random_ips(3))
45
46
  assert len(generate_random_ips(2, family=6)) == 2
46
- assert reverse_dns("0.0.0.0")["hostname"] is None
47
- assert len(reverse_dns(["0.0.0.0", "127.0.0.1"])) == 2
47
+ assert reverse_dns("0.0.0.0") is None
48
+ assert reverse_dns("0.0.0.0", only_hostnames=False)["hostname"] is None
49
+ assert all(item["hostname"] for item in reverse_dns(["0.0.0.0", "127.0.0.1"]))
48
50
  assert prompt("Hi {name}", name="Will") == "Hi Will"
49
51
  assert tool_schema("lookup_dns", "Lookup DNS", {"host": {"type": "string"}})["name"] == "lookup_dns"
File without changes
File without changes