shadow-tools 0.2.5__tar.gz → 0.3.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.
Files changed (34) hide show
  1. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/PKG-INFO +21 -1
  2. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/README.md +20 -0
  3. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/pyproject.toml +1 -1
  4. shadow_tools-0.3.0/release-0.2.5/shadow_tools-0.2.5-py3-none-any.whl +0 -0
  5. shadow_tools-0.3.0/release-0.2.5/shadow_tools-0.2.5.tar.gz +0 -0
  6. shadow_tools-0.3.0/src/shadowtools/__init__.py +20 -0
  7. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/cli.py +9 -0
  8. shadow_tools-0.3.0/src/shadowtools/config.py +23 -0
  9. shadow_tools-0.3.0/src/shadowtools/hashing.py +17 -0
  10. shadow_tools-0.3.0/src/shadowtools/ids.py +20 -0
  11. shadow_tools-0.3.0/src/shadowtools/retry.py +28 -0
  12. shadow_tools-0.3.0/src/shadowtools/text.py +33 -0
  13. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/tests/test_core.py +14 -0
  14. shadow_tools-0.2.5/src/shadowtools/__init__.py +0 -15
  15. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/.gitignore +0 -0
  16. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/LICENSE +0 -0
  17. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.0/shadow_tools-0.2.0-py3-none-any.whl +0 -0
  18. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.0/shadow_tools-0.2.0.tar.gz +0 -0
  19. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.1/shadow_tools-0.2.1-py3-none-any.whl +0 -0
  20. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.1/shadow_tools-0.2.1.tar.gz +0 -0
  21. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.2/shadow_tools-0.2.2-py3-none-any.whl +0 -0
  22. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.2/shadow_tools-0.2.2.tar.gz +0 -0
  23. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.3/shadow_tools-0.2.3-py3-none-any.whl +0 -0
  24. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.3/shadow_tools-0.2.3.tar.gz +0 -0
  25. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.4/shadow_tools-0.2.4-py3-none-any.whl +0 -0
  26. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/release-0.2.4/shadow_tools-0.2.4.tar.gz +0 -0
  27. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/ai.py +0 -0
  28. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/async_tools.py +0 -0
  29. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/data.py +0 -0
  30. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/files.py +0 -0
  31. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/http.py +0 -0
  32. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/network.py +0 -0
  33. {shadow_tools-0.2.5 → shadow_tools-0.3.0}/src/shadowtools/validation.py +0 -0
  34. {shadow_tools-0.2.5 → shadow_tools-0.3.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.2.5
3
+ Version: 0.3.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
@@ -43,6 +43,9 @@ pip install "shadow-tools[websocket]" # optional WebSocket client
43
43
  - Email, URL, and required-key checks
44
44
  - DNS lookup, subdomain parsing, and documentation-only test IPs
45
45
  - Provider-neutral AI prompt and tool-schema helpers
46
+ - File and text hashing
47
+ - UUIDs, secure tokens, and timestamp IDs
48
+ - Slugs, snake/camel case, chunks, environment variables, and retry logic
46
49
  - `shadow` command-line tool
47
50
 
48
51
  ## Quick start
@@ -94,6 +97,23 @@ search_tool = tool_schema(
94
97
  )
95
98
  ```
96
99
 
100
+ ## Everyday helpers
101
+
102
+ ```python
103
+ from shadowtools import hash_text, random_token, slugify, to_bool, uuid4
104
+
105
+ print(hash_text("hello"))
106
+ print(random_token())
107
+ print(uuid4())
108
+ print(slugify("Shadow Tools: Version 3!")) # shadow-tools-version-3
109
+ print(to_bool("yes")) # True
110
+ ```
111
+
112
+ ```bash
113
+ shadow hash settings.json
114
+ shadow uuid
115
+ ```
116
+
97
117
  ```bash
98
118
  shadow get https://api.github.com --json
99
119
  shadow json-format settings.json
@@ -19,6 +19,9 @@ pip install "shadow-tools[websocket]" # optional WebSocket client
19
19
  - Email, URL, and required-key checks
20
20
  - DNS lookup, subdomain parsing, and documentation-only test IPs
21
21
  - Provider-neutral AI prompt and tool-schema helpers
22
+ - File and text hashing
23
+ - UUIDs, secure tokens, and timestamp IDs
24
+ - Slugs, snake/camel case, chunks, environment variables, and retry logic
22
25
  - `shadow` command-line tool
23
26
 
24
27
  ## Quick start
@@ -70,6 +73,23 @@ search_tool = tool_schema(
70
73
  )
71
74
  ```
72
75
 
76
+ ## Everyday helpers
77
+
78
+ ```python
79
+ from shadowtools import hash_text, random_token, slugify, to_bool, uuid4
80
+
81
+ print(hash_text("hello"))
82
+ print(random_token())
83
+ print(uuid4())
84
+ print(slugify("Shadow Tools: Version 3!")) # shadow-tools-version-3
85
+ print(to_bool("yes")) # True
86
+ ```
87
+
88
+ ```bash
89
+ shadow hash settings.json
90
+ shadow uuid
91
+ ```
92
+
73
93
  ```bash
74
94
  shadow get https://api.github.com --json
75
95
  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.2.5"
7
+ version = "0.3.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"
@@ -0,0 +1,20 @@
1
+ """Shadow Tools: focused helpers for common Python automation tasks."""
2
+
3
+ from .data import read_csv, read_json, write_csv, write_json
4
+ from .config import env, require_env, to_bool
5
+ from .files import append_text, ensure_dir, read_text, write_text
6
+ from .hashing import file_hash, hash_text
7
+ from .http import get, request
8
+ from .ids import random_token, timestamp_id, uuid4
9
+ from .network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns, reverse_dns
10
+ from .retry import retry
11
+ from .text import chunks, slugify, to_camel, to_snake
12
+ from .validation import is_email, is_url, require_keys
13
+
14
+ __version__ = "0.3.0"
15
+
16
+ __all__ = [
17
+ "append_text", "chunks", "ensure_dir", "env", "file_hash", "generate_random_ips", "generate_test_ips", "get", "hash_text", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "random_token", "read_csv",
18
+ "read_json", "read_text", "request", "require_env", "require_keys", "retry", "reverse_dns", "resolve_dns", "slugify", "timestamp_id", "to_bool", "to_camel", "to_snake", "uuid4", "write_csv",
19
+ "write_json", "write_text",
20
+ ]
@@ -8,6 +8,8 @@ from pathlib import Path
8
8
  from . import __version__
9
9
  from .data import read_json, write_json
10
10
  from .http import get
11
+ from .hashing import file_hash
12
+ from .ids import uuid4
11
13
 
12
14
 
13
15
  def main() -> None:
@@ -19,6 +21,9 @@ def main() -> None:
19
21
  http_parser.add_argument("--json", action="store_true", help="Pretty-print a JSON response")
20
22
  json_parser = commands.add_parser("json-format", help="Format a JSON file")
21
23
  json_parser.add_argument("path", type=Path)
24
+ hash_parser = commands.add_parser("hash", help="Print a file SHA-256 checksum")
25
+ hash_parser.add_argument("path", type=Path)
26
+ commands.add_parser("uuid", help="Create a UUID")
22
27
  args = parser.parse_args()
23
28
  if args.command == "get":
24
29
  response = get(args.url)
@@ -27,6 +32,10 @@ def main() -> None:
27
32
  elif args.command == "json-format":
28
33
  write_json(args.path, read_json(args.path))
29
34
  print(f"Formatted {args.path}")
35
+ elif args.command == "hash":
36
+ print(file_hash(args.path))
37
+ elif args.command == "uuid":
38
+ print(uuid4())
30
39
 
31
40
 
32
41
  if __name__ == "__main__":
@@ -0,0 +1,23 @@
1
+ """Environment-variable helpers."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+
6
+
7
+ def env(name: str, default: str | None = None) -> str | None:
8
+ return os.environ.get(name, default)
9
+
10
+
11
+ def require_env(name: str) -> str:
12
+ value = os.environ.get(name)
13
+ if not value:
14
+ raise EnvironmentError(f"Required environment variable is missing: {name}")
15
+ return value
16
+
17
+
18
+ def to_bool(value: str | bool | int | None, default: bool = False) -> bool:
19
+ if value is None:
20
+ return default
21
+ if isinstance(value, bool):
22
+ return value
23
+ return str(value).strip().lower() in {"1", "true", "yes", "on", "y"}
@@ -0,0 +1,17 @@
1
+ """Checksums for text and files."""
2
+ from __future__ import annotations
3
+
4
+ import hashlib
5
+ from pathlib import Path
6
+
7
+
8
+ def hash_text(value: str, algorithm: str = "sha256") -> str:
9
+ return hashlib.new(algorithm, value.encode("utf-8")).hexdigest()
10
+
11
+
12
+ def file_hash(path: str | Path, algorithm: str = "sha256", *, chunk_size: int = 65_536) -> str:
13
+ digest = hashlib.new(algorithm)
14
+ with Path(path).open("rb") as handle:
15
+ for chunk in iter(lambda: handle.read(chunk_size), b""):
16
+ digest.update(chunk)
17
+ return digest.hexdigest()
@@ -0,0 +1,20 @@
1
+ """IDs and secure random tokens."""
2
+ from __future__ import annotations
3
+
4
+ import secrets
5
+ import time
6
+ import uuid
7
+
8
+
9
+ def uuid4() -> str:
10
+ return str(uuid.uuid4())
11
+
12
+
13
+ def random_token(length: int = 32) -> str:
14
+ if not 8 <= length <= 256:
15
+ raise ValueError("length must be between 8 and 256")
16
+ return secrets.token_urlsafe(length)[:length]
17
+
18
+
19
+ def timestamp_id(prefix: str = "item") -> str:
20
+ return f"{prefix}_{int(time.time() * 1000)}_{secrets.token_hex(3)}"
@@ -0,0 +1,28 @@
1
+ """Simple retry support for temporary failures."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ from collections.abc import Callable
6
+ from typing import ParamSpec, TypeVar
7
+
8
+ P = ParamSpec("P")
9
+ T = TypeVar("T")
10
+
11
+
12
+ def retry(attempts: int = 3, *, delay: float = 0.25,
13
+ exceptions: tuple[type[Exception], ...] = (Exception,)) -> Callable[[Callable[P, T]], Callable[P, T]]:
14
+ if attempts < 1:
15
+ raise ValueError("attempts must be at least 1")
16
+
17
+ def decorate(function: Callable[P, T]) -> Callable[P, T]:
18
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
19
+ for attempt in range(attempts):
20
+ try:
21
+ return function(*args, **kwargs)
22
+ except exceptions:
23
+ if attempt == attempts - 1:
24
+ raise
25
+ time.sleep(delay * (2 ** attempt))
26
+ raise RuntimeError("unreachable")
27
+ return wrapped
28
+ return decorate
@@ -0,0 +1,33 @@
1
+ """Plain text cleanup and naming helpers."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from collections.abc import Iterable
6
+ from typing import TypeVar
7
+
8
+ T = TypeVar("T")
9
+ _WORDS = re.compile(r"[A-Z]+(?=[A-Z][a-z])|[A-Z]?[a-z]+|[A-Z]+|\d+")
10
+
11
+
12
+ def _words(value: str) -> list[str]:
13
+ return _WORDS.findall(value.replace("-", " ").replace("_", " "))
14
+
15
+
16
+ def slugify(value: str) -> str:
17
+ return "-".join(word.lower() for word in _words(value))
18
+
19
+
20
+ def to_snake(value: str) -> str:
21
+ return "_".join(word.lower() for word in _words(value))
22
+
23
+
24
+ def to_camel(value: str) -> str:
25
+ words = _words(value)
26
+ return "".join(word[:1].upper() + word[1:].lower() for word in words)
27
+
28
+
29
+ def chunks(items: Iterable[T], size: int) -> list[list[T]]:
30
+ if size < 1:
31
+ raise ValueError("size must be at least 1")
32
+ values = list(items)
33
+ return [values[index:index + size] for index in range(0, len(values), size)]
@@ -8,6 +8,10 @@ from shadowtools.data import read_csv, read_json, write_csv, write_json
8
8
  from shadowtools.files import append_text, read_text, write_text
9
9
  from shadowtools.network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, reverse_dns
10
10
  from shadowtools.ai import prompt, tool_schema
11
+ from shadowtools.config import to_bool
12
+ from shadowtools.hashing import hash_text
13
+ from shadowtools.ids import random_token
14
+ from shadowtools.text import chunks, slugify, to_camel, to_snake
11
15
  from shadowtools.validation import is_email, is_url, require_keys
12
16
 
13
17
 
@@ -47,5 +51,15 @@ def test_network_and_ai_helpers():
47
51
  assert reverse_dns("0.0.0.0") is None
48
52
  assert reverse_dns("0.0.0.0", only_hostnames=False)["hostname"] is None
49
53
  assert all(item["hostname"] for item in reverse_dns(["0.0.0.0", "127.0.0.1"]))
54
+
55
+
56
+ def test_general_utilities():
57
+ assert hash_text("hello") == "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
58
+ assert len(random_token(16)) == 16
59
+ assert slugify("Shadow Tools V3") == "shadow-tools-v-3"
60
+ assert to_snake("ShadowTools") == "shadow_tools"
61
+ assert to_camel("shadow tools") == "ShadowTools"
62
+ assert chunks(range(5), 2) == [[0, 1], [2, 3], [4]]
63
+ assert to_bool("yes")
50
64
  assert prompt("Hi {name}", name="Will") == "Hi Will"
51
65
  assert tool_schema("lookup_dns", "Lookup DNS", {"host": {"type": "string"}})["name"] == "lookup_dns"
@@ -1,15 +0,0 @@
1
- """Shadow Tools: focused helpers for common Python automation tasks."""
2
-
3
- from .data import read_csv, read_json, write_csv, write_json
4
- from .files import append_text, ensure_dir, read_text, write_text
5
- from .http import get, request
6
- from .network import generate_random_ips, generate_test_ips, is_subdomain_of, make_subdomain, resolve_dns, reverse_dns
7
- from .validation import is_email, is_url, require_keys
8
-
9
- __version__ = "0.2.5"
10
-
11
- __all__ = [
12
- "append_text", "ensure_dir", "generate_random_ips", "generate_test_ips", "get", "is_email", "is_subdomain_of", "is_url", "make_subdomain", "read_csv",
13
- "read_json", "read_text", "request", "require_keys", "write_csv",
14
- "write_json", "write_text", "resolve_dns", "reverse_dns",
15
- ]
File without changes
File without changes