provide-foundation 0.0.0.dev0__py3-none-any.whl

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 (149) hide show
  1. provide/__init__.py +15 -0
  2. provide/foundation/__init__.py +155 -0
  3. provide/foundation/_version.py +58 -0
  4. provide/foundation/cli/__init__.py +67 -0
  5. provide/foundation/cli/commands/__init__.py +3 -0
  6. provide/foundation/cli/commands/deps.py +71 -0
  7. provide/foundation/cli/commands/logs/__init__.py +63 -0
  8. provide/foundation/cli/commands/logs/generate.py +357 -0
  9. provide/foundation/cli/commands/logs/generate_old.py +569 -0
  10. provide/foundation/cli/commands/logs/query.py +174 -0
  11. provide/foundation/cli/commands/logs/send.py +166 -0
  12. provide/foundation/cli/commands/logs/tail.py +112 -0
  13. provide/foundation/cli/decorators.py +262 -0
  14. provide/foundation/cli/main.py +65 -0
  15. provide/foundation/cli/testing.py +220 -0
  16. provide/foundation/cli/utils.py +210 -0
  17. provide/foundation/config/__init__.py +106 -0
  18. provide/foundation/config/base.py +295 -0
  19. provide/foundation/config/env.py +369 -0
  20. provide/foundation/config/loader.py +311 -0
  21. provide/foundation/config/manager.py +387 -0
  22. provide/foundation/config/schema.py +284 -0
  23. provide/foundation/config/sync.py +281 -0
  24. provide/foundation/config/types.py +78 -0
  25. provide/foundation/config/validators.py +80 -0
  26. provide/foundation/console/__init__.py +29 -0
  27. provide/foundation/console/input.py +364 -0
  28. provide/foundation/console/output.py +178 -0
  29. provide/foundation/context/__init__.py +12 -0
  30. provide/foundation/context/core.py +356 -0
  31. provide/foundation/core.py +20 -0
  32. provide/foundation/crypto/__init__.py +182 -0
  33. provide/foundation/crypto/algorithms.py +111 -0
  34. provide/foundation/crypto/certificates.py +896 -0
  35. provide/foundation/crypto/checksums.py +301 -0
  36. provide/foundation/crypto/constants.py +57 -0
  37. provide/foundation/crypto/hashing.py +265 -0
  38. provide/foundation/crypto/keys.py +188 -0
  39. provide/foundation/crypto/signatures.py +144 -0
  40. provide/foundation/crypto/utils.py +164 -0
  41. provide/foundation/errors/__init__.py +96 -0
  42. provide/foundation/errors/auth.py +73 -0
  43. provide/foundation/errors/base.py +81 -0
  44. provide/foundation/errors/config.py +103 -0
  45. provide/foundation/errors/context.py +299 -0
  46. provide/foundation/errors/decorators.py +484 -0
  47. provide/foundation/errors/handlers.py +360 -0
  48. provide/foundation/errors/integration.py +105 -0
  49. provide/foundation/errors/platform.py +37 -0
  50. provide/foundation/errors/process.py +140 -0
  51. provide/foundation/errors/resources.py +133 -0
  52. provide/foundation/errors/runtime.py +160 -0
  53. provide/foundation/errors/safe_decorators.py +133 -0
  54. provide/foundation/errors/types.py +276 -0
  55. provide/foundation/file/__init__.py +79 -0
  56. provide/foundation/file/atomic.py +157 -0
  57. provide/foundation/file/directory.py +134 -0
  58. provide/foundation/file/formats.py +236 -0
  59. provide/foundation/file/lock.py +175 -0
  60. provide/foundation/file/safe.py +179 -0
  61. provide/foundation/file/utils.py +170 -0
  62. provide/foundation/hub/__init__.py +88 -0
  63. provide/foundation/hub/click_builder.py +310 -0
  64. provide/foundation/hub/commands.py +42 -0
  65. provide/foundation/hub/components.py +640 -0
  66. provide/foundation/hub/decorators.py +244 -0
  67. provide/foundation/hub/info.py +32 -0
  68. provide/foundation/hub/manager.py +446 -0
  69. provide/foundation/hub/registry.py +279 -0
  70. provide/foundation/hub/type_mapping.py +54 -0
  71. provide/foundation/hub/types.py +28 -0
  72. provide/foundation/logger/__init__.py +41 -0
  73. provide/foundation/logger/base.py +22 -0
  74. provide/foundation/logger/config/__init__.py +16 -0
  75. provide/foundation/logger/config/base.py +40 -0
  76. provide/foundation/logger/config/logging.py +394 -0
  77. provide/foundation/logger/config/telemetry.py +188 -0
  78. provide/foundation/logger/core.py +239 -0
  79. provide/foundation/logger/custom_processors.py +172 -0
  80. provide/foundation/logger/emoji/__init__.py +44 -0
  81. provide/foundation/logger/emoji/matrix.py +209 -0
  82. provide/foundation/logger/emoji/sets.py +458 -0
  83. provide/foundation/logger/emoji/types.py +56 -0
  84. provide/foundation/logger/factories.py +56 -0
  85. provide/foundation/logger/processors/__init__.py +13 -0
  86. provide/foundation/logger/processors/main.py +254 -0
  87. provide/foundation/logger/processors/trace.py +113 -0
  88. provide/foundation/logger/ratelimit/__init__.py +31 -0
  89. provide/foundation/logger/ratelimit/limiters.py +294 -0
  90. provide/foundation/logger/ratelimit/processor.py +203 -0
  91. provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
  92. provide/foundation/logger/setup/__init__.py +29 -0
  93. provide/foundation/logger/setup/coordinator.py +138 -0
  94. provide/foundation/logger/setup/emoji_resolver.py +64 -0
  95. provide/foundation/logger/setup/processors.py +85 -0
  96. provide/foundation/logger/setup/testing.py +39 -0
  97. provide/foundation/logger/trace.py +38 -0
  98. provide/foundation/metrics/__init__.py +119 -0
  99. provide/foundation/metrics/otel.py +122 -0
  100. provide/foundation/metrics/simple.py +165 -0
  101. provide/foundation/observability/__init__.py +53 -0
  102. provide/foundation/observability/openobserve/__init__.py +79 -0
  103. provide/foundation/observability/openobserve/auth.py +72 -0
  104. provide/foundation/observability/openobserve/client.py +307 -0
  105. provide/foundation/observability/openobserve/commands.py +357 -0
  106. provide/foundation/observability/openobserve/exceptions.py +41 -0
  107. provide/foundation/observability/openobserve/formatters.py +298 -0
  108. provide/foundation/observability/openobserve/models.py +134 -0
  109. provide/foundation/observability/openobserve/otlp.py +320 -0
  110. provide/foundation/observability/openobserve/search.py +222 -0
  111. provide/foundation/observability/openobserve/streaming.py +235 -0
  112. provide/foundation/platform/__init__.py +44 -0
  113. provide/foundation/platform/detection.py +193 -0
  114. provide/foundation/platform/info.py +157 -0
  115. provide/foundation/process/__init__.py +39 -0
  116. provide/foundation/process/async_runner.py +373 -0
  117. provide/foundation/process/lifecycle.py +406 -0
  118. provide/foundation/process/runner.py +390 -0
  119. provide/foundation/setup/__init__.py +101 -0
  120. provide/foundation/streams/__init__.py +44 -0
  121. provide/foundation/streams/console.py +57 -0
  122. provide/foundation/streams/core.py +65 -0
  123. provide/foundation/streams/file.py +104 -0
  124. provide/foundation/testing/__init__.py +166 -0
  125. provide/foundation/testing/cli.py +227 -0
  126. provide/foundation/testing/crypto.py +163 -0
  127. provide/foundation/testing/fixtures.py +49 -0
  128. provide/foundation/testing/hub.py +23 -0
  129. provide/foundation/testing/logger.py +106 -0
  130. provide/foundation/testing/streams.py +54 -0
  131. provide/foundation/tracer/__init__.py +49 -0
  132. provide/foundation/tracer/context.py +115 -0
  133. provide/foundation/tracer/otel.py +135 -0
  134. provide/foundation/tracer/spans.py +174 -0
  135. provide/foundation/types.py +32 -0
  136. provide/foundation/utils/__init__.py +97 -0
  137. provide/foundation/utils/deps.py +195 -0
  138. provide/foundation/utils/env.py +491 -0
  139. provide/foundation/utils/formatting.py +483 -0
  140. provide/foundation/utils/parsing.py +235 -0
  141. provide/foundation/utils/rate_limiting.py +112 -0
  142. provide/foundation/utils/streams.py +67 -0
  143. provide/foundation/utils/timing.py +93 -0
  144. provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
  145. provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
  146. provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
  147. provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
  148. provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
  149. provide_foundation-0.0.0.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,301 @@
1
+ """Checksum verification and management."""
2
+
3
+ from pathlib import Path
4
+
5
+ from provide.foundation.crypto.algorithms import DEFAULT_ALGORITHM
6
+ from provide.foundation.crypto.hashing import hash_data, hash_file
7
+ from provide.foundation.crypto.utils import compare_hash
8
+ from provide.foundation.errors.resources import ResourceError
9
+ from provide.foundation.logger import get_logger
10
+
11
+ plog = get_logger(__name__)
12
+
13
+
14
+ def verify_file(
15
+ path: Path | str,
16
+ expected_hash: str,
17
+ algorithm: str = DEFAULT_ALGORITHM,
18
+ ) -> bool:
19
+ """Verify a file matches an expected hash.
20
+
21
+ Args:
22
+ path: File path
23
+ expected_hash: Expected hash value
24
+ algorithm: Hash algorithm
25
+
26
+ Returns:
27
+ True if hash matches, False otherwise
28
+
29
+ Raises:
30
+ ResourceError: If file cannot be read
31
+ ValidationError: If algorithm is not supported
32
+ """
33
+ if isinstance(path, str):
34
+ path = Path(path)
35
+
36
+ try:
37
+ actual_hash = hash_file(path, algorithm)
38
+ matches = compare_hash(actual_hash, expected_hash)
39
+
40
+ if matches:
41
+ plog.debug(
42
+ "✅ Checksum verified",
43
+ path=str(path),
44
+ algorithm=algorithm,
45
+ )
46
+ else:
47
+ plog.warning(
48
+ "❌ Checksum mismatch",
49
+ path=str(path),
50
+ algorithm=algorithm,
51
+ expected=expected_hash[:16] + "...",
52
+ actual=actual_hash[:16] + "...",
53
+ )
54
+
55
+ return matches
56
+
57
+ except ResourceError:
58
+ plog.error(
59
+ "❌ Failed to verify checksum - file not found",
60
+ path=str(path),
61
+ )
62
+ return False
63
+
64
+
65
+ def verify_data(
66
+ data: bytes,
67
+ expected_hash: str,
68
+ algorithm: str = DEFAULT_ALGORITHM,
69
+ ) -> bool:
70
+ """Verify data matches an expected hash.
71
+
72
+ Args:
73
+ data: Data to verify
74
+ expected_hash: Expected hash value
75
+ algorithm: Hash algorithm
76
+
77
+ Returns:
78
+ True if hash matches, False otherwise
79
+
80
+ Raises:
81
+ ValidationError: If algorithm is not supported
82
+ """
83
+ actual_hash = hash_data(data, algorithm)
84
+ matches = compare_hash(actual_hash, expected_hash)
85
+
86
+ if matches:
87
+ plog.debug(
88
+ "✅ Data checksum verified",
89
+ algorithm=algorithm,
90
+ size=len(data),
91
+ )
92
+ else:
93
+ plog.warning(
94
+ "❌ Data checksum mismatch",
95
+ algorithm=algorithm,
96
+ expected=expected_hash[:16] + "...",
97
+ actual=actual_hash[:16] + "...",
98
+ )
99
+
100
+ return matches
101
+
102
+
103
+ def calculate_checksums(
104
+ path: Path | str,
105
+ algorithms: list[str] | None = None,
106
+ ) -> dict[str, str]:
107
+ """Calculate multiple checksums for a file.
108
+
109
+ Args:
110
+ path: File path
111
+ algorithms: List of algorithms (defaults to sha256 and md5)
112
+
113
+ Returns:
114
+ Dictionary mapping algorithm name to hex digest
115
+
116
+ Raises:
117
+ ResourceError: If file cannot be read
118
+ ValidationError: If any algorithm is not supported
119
+ """
120
+ if algorithms is None:
121
+ algorithms = ["sha256", "md5"]
122
+
123
+ from provide.foundation.crypto.hashing import hash_file_multiple
124
+
125
+ checksums = hash_file_multiple(path, algorithms)
126
+
127
+ plog.debug(
128
+ "📝 Calculated checksums",
129
+ path=str(path),
130
+ algorithms=algorithms,
131
+ )
132
+
133
+ return checksums
134
+
135
+
136
+ def parse_checksum_file(
137
+ path: Path | str,
138
+ algorithm: str | None = None,
139
+ ) -> dict[str, str]:
140
+ """Parse a checksum file and return filename to hash mapping.
141
+
142
+ Supports common checksum file formats:
143
+ - SHA256: "hash filename" or "hash filename"
144
+ - MD5: "hash filename" or "hash filename"
145
+ - SHA256SUMS: "hash filename"
146
+ - MD5SUMS: "hash filename"
147
+
148
+ Args:
149
+ path: Path to checksum file
150
+ algorithm: Expected algorithm (for validation)
151
+
152
+ Returns:
153
+ Dictionary mapping filename to hash
154
+
155
+ Raises:
156
+ ResourceError: If file cannot be read
157
+ """
158
+ if isinstance(path, str):
159
+ path = Path(path)
160
+
161
+ if not path.exists():
162
+ raise ResourceError(
163
+ f"Checksum file not found: {path}",
164
+ resource_type="file",
165
+ resource_path=str(path),
166
+ )
167
+
168
+ checksums = {}
169
+
170
+ try:
171
+ with open(path, encoding="utf-8") as f:
172
+ for line in f:
173
+ line = line.strip()
174
+ if not line or line.startswith("#"):
175
+ continue
176
+
177
+ # Split on whitespace (handle both single and double space)
178
+ parts = line.split(None, 1)
179
+ if len(parts) == 2:
180
+ hash_value, filename = parts
181
+ # Remove any leading asterisk (binary mode indicator)
182
+ if filename.startswith("*"):
183
+ filename = filename[1:]
184
+ checksums[filename] = hash_value.lower()
185
+
186
+ plog.debug(
187
+ "📄 Parsed checksum file",
188
+ path=str(path),
189
+ entries=len(checksums),
190
+ algorithm=algorithm,
191
+ )
192
+
193
+ return checksums
194
+
195
+ except OSError as e:
196
+ raise ResourceError(
197
+ f"Failed to read checksum file: {path}",
198
+ resource_type="file",
199
+ resource_path=str(path),
200
+ ) from e
201
+
202
+
203
+ def write_checksum_file(
204
+ checksums: dict[str, str],
205
+ path: Path | str,
206
+ algorithm: str = DEFAULT_ALGORITHM,
207
+ binary_mode: bool = True,
208
+ ) -> None:
209
+ """Write checksums to a file in standard format.
210
+
211
+ Args:
212
+ checksums: Dictionary mapping filename to hash
213
+ path: Path to write checksum file
214
+ algorithm: Algorithm name (for comments)
215
+ binary_mode: Whether to use binary mode indicator (*)
216
+
217
+ Raises:
218
+ ResourceError: If file cannot be written
219
+ """
220
+ if isinstance(path, str):
221
+ path = Path(path)
222
+
223
+ try:
224
+ with open(path, "w", encoding="utf-8") as f:
225
+ # Write header comment
226
+ f.write(f"# {algorithm.upper()} checksums\n")
227
+ f.write("# Generated by provide.foundation\n\n")
228
+
229
+ # Write checksums
230
+ for filename, hash_value in sorted(checksums.items()):
231
+ if binary_mode:
232
+ f.write(f"{hash_value} *{filename}\n")
233
+ else:
234
+ f.write(f"{hash_value} {filename}\n")
235
+
236
+ plog.debug(
237
+ "📝 Wrote checksum file",
238
+ path=str(path),
239
+ entries=len(checksums),
240
+ algorithm=algorithm,
241
+ )
242
+
243
+ except OSError as e:
244
+ raise ResourceError(
245
+ f"Failed to write checksum file: {path}",
246
+ resource_type="file",
247
+ resource_path=str(path),
248
+ ) from e
249
+
250
+
251
+ def verify_checksum_file(
252
+ checksum_file: Path | str,
253
+ base_dir: Path | str | None = None,
254
+ algorithm: str = DEFAULT_ALGORITHM,
255
+ stop_on_error: bool = False,
256
+ ) -> tuple[list[str], list[str]]:
257
+ """Verify all files listed in a checksum file.
258
+
259
+ Args:
260
+ checksum_file: Path to checksum file
261
+ base_dir: Base directory for relative paths (defaults to checksum file dir)
262
+ algorithm: Hash algorithm to use
263
+ stop_on_error: Whether to stop on first verification failure
264
+
265
+ Returns:
266
+ Tuple of (verified_files, failed_files)
267
+
268
+ Raises:
269
+ ResourceError: If checksum file cannot be read
270
+ """
271
+ if isinstance(checksum_file, str):
272
+ checksum_file = Path(checksum_file)
273
+
274
+ if base_dir is None:
275
+ base_dir = checksum_file.parent
276
+ elif isinstance(base_dir, str):
277
+ base_dir = Path(base_dir)
278
+
279
+ checksums = parse_checksum_file(checksum_file, algorithm)
280
+
281
+ verified = []
282
+ failed = []
283
+
284
+ for filename, expected_hash in checksums.items():
285
+ file_path = base_dir / filename
286
+
287
+ if verify_file(file_path, expected_hash, algorithm):
288
+ verified.append(filename)
289
+ else:
290
+ failed.append(filename)
291
+ if stop_on_error:
292
+ break
293
+
294
+ plog.info(
295
+ "📊 Checksum verification complete",
296
+ verified=len(verified),
297
+ failed=len(failed),
298
+ total=len(checksums),
299
+ )
300
+
301
+ return verified, failed
@@ -0,0 +1,57 @@
1
+ """Cryptographic constants and configuration."""
2
+
3
+ from typing import Final
4
+
5
+ # Ed25519 constants
6
+ ED25519_PRIVATE_KEY_SIZE: Final[int] = 32
7
+ ED25519_PUBLIC_KEY_SIZE: Final[int] = 32
8
+ ED25519_SIGNATURE_SIZE: Final[int] = 64
9
+
10
+ # RSA key sizes
11
+ DEFAULT_RSA_KEY_SIZE: Final[int] = 2048
12
+ SUPPORTED_RSA_SIZES: Final[set[int]] = {2048, 3072, 4096}
13
+
14
+ # ECDSA curves
15
+ DEFAULT_ECDSA_CURVE: Final[str] = "secp384r1"
16
+ SUPPORTED_EC_CURVES: Final[set[str]] = {
17
+ "secp256r1",
18
+ "secp384r1",
19
+ "secp521r1",
20
+ }
21
+
22
+ # Key types
23
+ SUPPORTED_KEY_TYPES: Final[set[str]] = {"rsa", "ecdsa", "ed25519"}
24
+
25
+ # Default algorithms for different use cases
26
+ DEFAULT_SIGNATURE_ALGORITHM: Final[str] = "ed25519" # Modern default for new code
27
+ DEFAULT_CERTIFICATE_KEY_TYPE: Final[str] = "ecdsa" # Good balance for TLS/PKI
28
+ DEFAULT_CERTIFICATE_CURVE: Final[str] = DEFAULT_ECDSA_CURVE
29
+
30
+ # Certificate defaults
31
+ DEFAULT_CERTIFICATE_VALIDITY_DAYS: Final[int] = 365
32
+ MIN_CERTIFICATE_VALIDITY_DAYS: Final[int] = 1
33
+ MAX_CERTIFICATE_VALIDITY_DAYS: Final[int] = 3650 # 10 years
34
+
35
+
36
+ # Optional config integration
37
+ def _get_config_value(key: str, default: str | int) -> str | int:
38
+ """Get crypto config value with fallback to default."""
39
+ try:
40
+ from provide.foundation.config import get_config
41
+
42
+ return get_config(f"crypto.{key}", default)
43
+ except ImportError:
44
+ # Config system not available, use defaults
45
+ return default
46
+
47
+
48
+ def get_default_hash_algorithm() -> str:
49
+ """Get default hash algorithm from config or fallback."""
50
+ from provide.foundation.crypto.algorithms import DEFAULT_ALGORITHM
51
+
52
+ return str(_get_config_value("hash_algorithm", DEFAULT_ALGORITHM))
53
+
54
+
55
+ def get_default_signature_algorithm() -> str:
56
+ """Get default signature algorithm from config or fallback."""
57
+ return str(_get_config_value("signature_algorithm", DEFAULT_SIGNATURE_ALGORITHM))
@@ -0,0 +1,265 @@
1
+ """Core hashing operations."""
2
+
3
+ from collections.abc import Iterator
4
+ from pathlib import Path
5
+ from typing import BinaryIO
6
+
7
+ from provide.foundation.crypto.algorithms import (
8
+ DEFAULT_ALGORITHM,
9
+ get_hasher,
10
+ validate_algorithm,
11
+ )
12
+ from provide.foundation.errors.resources import ResourceError
13
+ from provide.foundation.logger import get_logger
14
+
15
+ plog = get_logger(__name__)
16
+
17
+ # Default chunk size for file reading (8KB)
18
+ DEFAULT_CHUNK_SIZE = 8192
19
+
20
+
21
+ def hash_file(
22
+ path: Path | str,
23
+ algorithm: str = DEFAULT_ALGORITHM,
24
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
25
+ ) -> str:
26
+ """Hash a file's contents.
27
+
28
+ Args:
29
+ path: File path
30
+ algorithm: Hash algorithm (sha256, sha512, md5, etc.)
31
+ chunk_size: Size of chunks to read at a time
32
+
33
+ Returns:
34
+ Hex digest of file hash
35
+
36
+ Raises:
37
+ ResourceError: If file cannot be read
38
+ ValidationError: If algorithm is not supported
39
+ """
40
+ if isinstance(path, str):
41
+ path = Path(path)
42
+
43
+ if not path.exists():
44
+ raise ResourceError(
45
+ f"File not found: {path}",
46
+ resource_type="file",
47
+ resource_path=str(path),
48
+ )
49
+
50
+ if not path.is_file():
51
+ raise ResourceError(
52
+ f"Path is not a file: {path}",
53
+ resource_type="file",
54
+ resource_path=str(path),
55
+ )
56
+
57
+ validate_algorithm(algorithm)
58
+ hasher = get_hasher(algorithm)
59
+
60
+ try:
61
+ with open(path, "rb") as f:
62
+ while chunk := f.read(chunk_size):
63
+ hasher.update(chunk)
64
+
65
+ hash_value = hasher.hexdigest()
66
+ plog.debug(
67
+ "🔐 Hashed file",
68
+ path=str(path),
69
+ algorithm=algorithm,
70
+ hash=hash_value[:16] + "...",
71
+ )
72
+ return hash_value
73
+
74
+ except OSError as e:
75
+ raise ResourceError(
76
+ f"Failed to read file: {path}",
77
+ resource_type="file",
78
+ resource_path=str(path),
79
+ ) from e
80
+
81
+
82
+ def hash_data(
83
+ data: bytes,
84
+ algorithm: str = DEFAULT_ALGORITHM,
85
+ ) -> str:
86
+ """Hash binary data.
87
+
88
+ Args:
89
+ data: Data to hash
90
+ algorithm: Hash algorithm
91
+
92
+ Returns:
93
+ Hex digest
94
+
95
+ Raises:
96
+ ValidationError: If algorithm is not supported
97
+ """
98
+ validate_algorithm(algorithm)
99
+ hasher = get_hasher(algorithm)
100
+ hasher.update(data)
101
+
102
+ hash_value = hasher.hexdigest()
103
+ plog.debug(
104
+ "🔐 Hashed data",
105
+ algorithm=algorithm,
106
+ size=len(data),
107
+ hash=hash_value[:16] + "...",
108
+ )
109
+ return hash_value
110
+
111
+
112
+ def hash_string(
113
+ text: str,
114
+ algorithm: str = DEFAULT_ALGORITHM,
115
+ encoding: str = "utf-8",
116
+ ) -> str:
117
+ """Hash a text string.
118
+
119
+ Args:
120
+ text: Text to hash
121
+ algorithm: Hash algorithm
122
+ encoding: Text encoding
123
+
124
+ Returns:
125
+ Hex digest
126
+
127
+ Raises:
128
+ ValidationError: If algorithm is not supported
129
+ """
130
+ return hash_data(text.encode(encoding), algorithm)
131
+
132
+
133
+ def hash_stream(
134
+ stream: BinaryIO,
135
+ algorithm: str = DEFAULT_ALGORITHM,
136
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
137
+ ) -> str:
138
+ """Hash data from a stream.
139
+
140
+ Args:
141
+ stream: Binary stream to read from
142
+ algorithm: Hash algorithm
143
+ chunk_size: Size of chunks to read at a time
144
+
145
+ Returns:
146
+ Hex digest
147
+
148
+ Raises:
149
+ ValidationError: If algorithm is not supported
150
+ """
151
+ validate_algorithm(algorithm)
152
+ hasher = get_hasher(algorithm)
153
+
154
+ bytes_read = 0
155
+ while chunk := stream.read(chunk_size):
156
+ hasher.update(chunk)
157
+ bytes_read += len(chunk)
158
+
159
+ hash_value = hasher.hexdigest()
160
+ plog.debug(
161
+ "🔐 Hashed stream",
162
+ algorithm=algorithm,
163
+ bytes_read=bytes_read,
164
+ hash=hash_value[:16] + "...",
165
+ )
166
+ return hash_value
167
+
168
+
169
+ def hash_file_multiple(
170
+ path: Path | str,
171
+ algorithms: list[str],
172
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
173
+ ) -> dict[str, str]:
174
+ """Hash a file with multiple algorithms in a single pass.
175
+
176
+ This is more efficient than calling hash_file multiple times.
177
+
178
+ Args:
179
+ path: File path
180
+ algorithms: List of hash algorithms
181
+ chunk_size: Size of chunks to read at a time
182
+
183
+ Returns:
184
+ Dictionary mapping algorithm name to hex digest
185
+
186
+ Raises:
187
+ ResourceError: If file cannot be read
188
+ ValidationError: If any algorithm is not supported
189
+ """
190
+ if isinstance(path, str):
191
+ path = Path(path)
192
+
193
+ if not path.exists():
194
+ raise ResourceError(
195
+ f"File not found: {path}",
196
+ resource_type="file",
197
+ resource_path=str(path),
198
+ )
199
+
200
+ # Create hashers for all algorithms
201
+ hashers = {}
202
+ for algo in algorithms:
203
+ validate_algorithm(algo)
204
+ hashers[algo] = get_hasher(algo)
205
+
206
+ # Read file once and update all hashers
207
+ try:
208
+ with open(path, "rb") as f:
209
+ while chunk := f.read(chunk_size):
210
+ for hasher in hashers.values():
211
+ hasher.update(chunk)
212
+
213
+ # Get results
214
+ results = {algo: hasher.hexdigest() for algo, hasher in hashers.items()}
215
+
216
+ plog.debug(
217
+ "🔐 Hashed file with multiple algorithms",
218
+ path=str(path),
219
+ algorithms=algorithms,
220
+ )
221
+
222
+ return results
223
+
224
+ except OSError as e:
225
+ raise ResourceError(
226
+ f"Failed to read file: {path}",
227
+ resource_type="file",
228
+ resource_path=str(path),
229
+ ) from e
230
+
231
+
232
+ def hash_chunks(
233
+ chunks: Iterator[bytes],
234
+ algorithm: str = DEFAULT_ALGORITHM,
235
+ ) -> str:
236
+ """Hash an iterator of byte chunks.
237
+
238
+ Useful for hashing data that comes in chunks, like from a network stream.
239
+
240
+ Args:
241
+ chunks: Iterator yielding byte chunks
242
+ algorithm: Hash algorithm
243
+
244
+ Returns:
245
+ Hex digest
246
+
247
+ Raises:
248
+ ValidationError: If algorithm is not supported
249
+ """
250
+ validate_algorithm(algorithm)
251
+ hasher = get_hasher(algorithm)
252
+
253
+ bytes_processed = 0
254
+ for chunk in chunks:
255
+ hasher.update(chunk)
256
+ bytes_processed += len(chunk)
257
+
258
+ hash_value = hasher.hexdigest()
259
+ plog.debug(
260
+ "🔐 Hashed chunks",
261
+ algorithm=algorithm,
262
+ bytes_processed=bytes_processed,
263
+ hash=hash_value[:16] + "...",
264
+ )
265
+ return hash_value