shadow-tools 0.1.0__tar.gz → 0.2.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.
- shadow_tools-0.2.0/.gitignore +6 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/PKG-INFO +25 -1
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/README.md +24 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/pyproject.toml +1 -1
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/__init__.py +4 -3
- shadow_tools-0.2.0/src/shadowtools/ai.py +29 -0
- shadow_tools-0.2.0/src/shadowtools/network.py +41 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/tests/test_core.py +10 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/LICENSE +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/async_tools.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/cli.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/data.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/files.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/http.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/validation.py +0 -0
- {shadow_tools-0.1.0 → shadow_tools-0.2.0}/src/shadowtools/websocket.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: shadow-tools
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
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
|
|
@@ -41,6 +41,8 @@ pip install "shadow-tools[websocket]" # optional WebSocket client
|
|
|
41
41
|
- File and directory helpers
|
|
42
42
|
- JSON and CSV reading/writing
|
|
43
43
|
- Email, URL, and required-key checks
|
|
44
|
+
- DNS lookup, subdomain parsing, and documentation-only test IPs
|
|
45
|
+
- Provider-neutral AI prompt and tool-schema helpers
|
|
44
46
|
- `shadow` command-line tool
|
|
45
47
|
|
|
46
48
|
## Quick start
|
|
@@ -54,6 +56,28 @@ write_json("output.json", {"ready": True})
|
|
|
54
56
|
assert is_email("person@example.com")
|
|
55
57
|
```
|
|
56
58
|
|
|
59
|
+
## DNS, subdomains, and test IPs
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from shadowtools import generate_test_ips, make_subdomain, resolve_dns
|
|
63
|
+
|
|
64
|
+
print(make_subdomain("api", "example.com")) # api.example.com
|
|
65
|
+
print(resolve_dns("example.com"))
|
|
66
|
+
print(generate_test_ips(2)) # safe documentation-only addresses
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## AI app helpers
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from shadowtools.ai import prompt, tool_schema
|
|
73
|
+
|
|
74
|
+
message = prompt("Explain {topic} in {words} words.", topic="DNS", words=50)
|
|
75
|
+
search_tool = tool_schema(
|
|
76
|
+
"search_docs", "Search internal documentation",
|
|
77
|
+
{"query": {"type": "string"}}, required=["query"]
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
57
81
|
```bash
|
|
58
82
|
shadow get https://api.github.com --json
|
|
59
83
|
shadow json-format settings.json
|
|
@@ -17,6 +17,8 @@ pip install "shadow-tools[websocket]" # optional WebSocket client
|
|
|
17
17
|
- File and directory helpers
|
|
18
18
|
- JSON and CSV reading/writing
|
|
19
19
|
- Email, URL, and required-key checks
|
|
20
|
+
- DNS lookup, subdomain parsing, and documentation-only test IPs
|
|
21
|
+
- Provider-neutral AI prompt and tool-schema helpers
|
|
20
22
|
- `shadow` command-line tool
|
|
21
23
|
|
|
22
24
|
## Quick start
|
|
@@ -30,6 +32,28 @@ write_json("output.json", {"ready": True})
|
|
|
30
32
|
assert is_email("person@example.com")
|
|
31
33
|
```
|
|
32
34
|
|
|
35
|
+
## DNS, subdomains, and test IPs
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from shadowtools import generate_test_ips, make_subdomain, resolve_dns
|
|
39
|
+
|
|
40
|
+
print(make_subdomain("api", "example.com")) # api.example.com
|
|
41
|
+
print(resolve_dns("example.com"))
|
|
42
|
+
print(generate_test_ips(2)) # safe documentation-only addresses
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## AI app helpers
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from shadowtools.ai import prompt, tool_schema
|
|
49
|
+
|
|
50
|
+
message = prompt("Explain {topic} in {words} words.", topic="DNS", words=50)
|
|
51
|
+
search_tool = tool_schema(
|
|
52
|
+
"search_docs", "Search internal documentation",
|
|
53
|
+
{"query": {"type": "string"}}, required=["query"]
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
33
57
|
```bash
|
|
34
58
|
shadow get https://api.github.com --json
|
|
35
59
|
shadow json-format settings.json
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shadow-tools"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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,12 +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
7
|
from .validation import is_email, is_url, require_keys
|
|
7
8
|
|
|
8
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.2.0"
|
|
9
10
|
|
|
10
11
|
__all__ = [
|
|
11
|
-
"append_text", "ensure_dir", "get", "is_email", "is_url", "read_csv",
|
|
12
|
+
"append_text", "ensure_dir", "generate_test_ips", "get", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "read_csv",
|
|
12
13
|
"read_json", "read_text", "request", "require_keys", "write_csv",
|
|
13
|
-
"write_json", "write_text",
|
|
14
|
+
"write_json", "write_text", "resolve_dns",
|
|
14
15
|
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Provider-neutral building blocks for AI application code."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def prompt(template: str, /, **values: object) -> str:
|
|
8
|
+
"""Fill a named prompt template and flag any missing variables clearly."""
|
|
9
|
+
try:
|
|
10
|
+
return template.format(**values)
|
|
11
|
+
except KeyError as exc:
|
|
12
|
+
raise ValueError(f"Missing prompt value: {exc.args[0]}") from exc
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tool_schema(name: str, description: str, properties: Mapping[str, Any],
|
|
16
|
+
required: list[str] | None = None) -> dict[str, Any]:
|
|
17
|
+
"""Create a compact JSON-schema function definition for AI tool calling."""
|
|
18
|
+
if not name.replace("_", "").isalnum():
|
|
19
|
+
raise ValueError("name may contain only letters, numbers, and underscores")
|
|
20
|
+
return {
|
|
21
|
+
"name": name,
|
|
22
|
+
"description": description,
|
|
23
|
+
"parameters": {
|
|
24
|
+
"type": "object",
|
|
25
|
+
"properties": dict(properties),
|
|
26
|
+
"required": required or [],
|
|
27
|
+
"additionalProperties": False,
|
|
28
|
+
},
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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)]
|
|
@@ -5,6 +5,8 @@ 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
|
|
9
|
+
from shadowtools.ai import prompt, tool_schema
|
|
8
10
|
from shadowtools.validation import is_email, is_url, require_keys
|
|
9
11
|
|
|
10
12
|
|
|
@@ -33,3 +35,11 @@ def test_validation():
|
|
|
33
35
|
|
|
34
36
|
def test_run_all():
|
|
35
37
|
assert asyncio.run(run_all([lambda: 2, lambda: 3])) == [2, 3]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_network_and_ai_helpers():
|
|
41
|
+
assert make_subdomain("API", "Example.COM") == "api.example.com"
|
|
42
|
+
assert is_subdomain_of("api.example.com", "example.com")
|
|
43
|
+
assert generate_test_ips(1)[0].startswith("192.0.2.")
|
|
44
|
+
assert prompt("Hi {name}", name="Will") == "Hi Will"
|
|
45
|
+
assert tool_schema("lookup_dns", "Lookup DNS", {"host": {"type": "string"}})["name"] == "lookup_dns"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|