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.
- provide/__init__.py +15 -0
- provide/foundation/__init__.py +155 -0
- provide/foundation/_version.py +58 -0
- provide/foundation/cli/__init__.py +67 -0
- provide/foundation/cli/commands/__init__.py +3 -0
- provide/foundation/cli/commands/deps.py +71 -0
- provide/foundation/cli/commands/logs/__init__.py +63 -0
- provide/foundation/cli/commands/logs/generate.py +357 -0
- provide/foundation/cli/commands/logs/generate_old.py +569 -0
- provide/foundation/cli/commands/logs/query.py +174 -0
- provide/foundation/cli/commands/logs/send.py +166 -0
- provide/foundation/cli/commands/logs/tail.py +112 -0
- provide/foundation/cli/decorators.py +262 -0
- provide/foundation/cli/main.py +65 -0
- provide/foundation/cli/testing.py +220 -0
- provide/foundation/cli/utils.py +210 -0
- provide/foundation/config/__init__.py +106 -0
- provide/foundation/config/base.py +295 -0
- provide/foundation/config/env.py +369 -0
- provide/foundation/config/loader.py +311 -0
- provide/foundation/config/manager.py +387 -0
- provide/foundation/config/schema.py +284 -0
- provide/foundation/config/sync.py +281 -0
- provide/foundation/config/types.py +78 -0
- provide/foundation/config/validators.py +80 -0
- provide/foundation/console/__init__.py +29 -0
- provide/foundation/console/input.py +364 -0
- provide/foundation/console/output.py +178 -0
- provide/foundation/context/__init__.py +12 -0
- provide/foundation/context/core.py +356 -0
- provide/foundation/core.py +20 -0
- provide/foundation/crypto/__init__.py +182 -0
- provide/foundation/crypto/algorithms.py +111 -0
- provide/foundation/crypto/certificates.py +896 -0
- provide/foundation/crypto/checksums.py +301 -0
- provide/foundation/crypto/constants.py +57 -0
- provide/foundation/crypto/hashing.py +265 -0
- provide/foundation/crypto/keys.py +188 -0
- provide/foundation/crypto/signatures.py +144 -0
- provide/foundation/crypto/utils.py +164 -0
- provide/foundation/errors/__init__.py +96 -0
- provide/foundation/errors/auth.py +73 -0
- provide/foundation/errors/base.py +81 -0
- provide/foundation/errors/config.py +103 -0
- provide/foundation/errors/context.py +299 -0
- provide/foundation/errors/decorators.py +484 -0
- provide/foundation/errors/handlers.py +360 -0
- provide/foundation/errors/integration.py +105 -0
- provide/foundation/errors/platform.py +37 -0
- provide/foundation/errors/process.py +140 -0
- provide/foundation/errors/resources.py +133 -0
- provide/foundation/errors/runtime.py +160 -0
- provide/foundation/errors/safe_decorators.py +133 -0
- provide/foundation/errors/types.py +276 -0
- provide/foundation/file/__init__.py +79 -0
- provide/foundation/file/atomic.py +157 -0
- provide/foundation/file/directory.py +134 -0
- provide/foundation/file/formats.py +236 -0
- provide/foundation/file/lock.py +175 -0
- provide/foundation/file/safe.py +179 -0
- provide/foundation/file/utils.py +170 -0
- provide/foundation/hub/__init__.py +88 -0
- provide/foundation/hub/click_builder.py +310 -0
- provide/foundation/hub/commands.py +42 -0
- provide/foundation/hub/components.py +640 -0
- provide/foundation/hub/decorators.py +244 -0
- provide/foundation/hub/info.py +32 -0
- provide/foundation/hub/manager.py +446 -0
- provide/foundation/hub/registry.py +279 -0
- provide/foundation/hub/type_mapping.py +54 -0
- provide/foundation/hub/types.py +28 -0
- provide/foundation/logger/__init__.py +41 -0
- provide/foundation/logger/base.py +22 -0
- provide/foundation/logger/config/__init__.py +16 -0
- provide/foundation/logger/config/base.py +40 -0
- provide/foundation/logger/config/logging.py +394 -0
- provide/foundation/logger/config/telemetry.py +188 -0
- provide/foundation/logger/core.py +239 -0
- provide/foundation/logger/custom_processors.py +172 -0
- provide/foundation/logger/emoji/__init__.py +44 -0
- provide/foundation/logger/emoji/matrix.py +209 -0
- provide/foundation/logger/emoji/sets.py +458 -0
- provide/foundation/logger/emoji/types.py +56 -0
- provide/foundation/logger/factories.py +56 -0
- provide/foundation/logger/processors/__init__.py +13 -0
- provide/foundation/logger/processors/main.py +254 -0
- provide/foundation/logger/processors/trace.py +113 -0
- provide/foundation/logger/ratelimit/__init__.py +31 -0
- provide/foundation/logger/ratelimit/limiters.py +294 -0
- provide/foundation/logger/ratelimit/processor.py +203 -0
- provide/foundation/logger/ratelimit/queue_limiter.py +305 -0
- provide/foundation/logger/setup/__init__.py +29 -0
- provide/foundation/logger/setup/coordinator.py +138 -0
- provide/foundation/logger/setup/emoji_resolver.py +64 -0
- provide/foundation/logger/setup/processors.py +85 -0
- provide/foundation/logger/setup/testing.py +39 -0
- provide/foundation/logger/trace.py +38 -0
- provide/foundation/metrics/__init__.py +119 -0
- provide/foundation/metrics/otel.py +122 -0
- provide/foundation/metrics/simple.py +165 -0
- provide/foundation/observability/__init__.py +53 -0
- provide/foundation/observability/openobserve/__init__.py +79 -0
- provide/foundation/observability/openobserve/auth.py +72 -0
- provide/foundation/observability/openobserve/client.py +307 -0
- provide/foundation/observability/openobserve/commands.py +357 -0
- provide/foundation/observability/openobserve/exceptions.py +41 -0
- provide/foundation/observability/openobserve/formatters.py +298 -0
- provide/foundation/observability/openobserve/models.py +134 -0
- provide/foundation/observability/openobserve/otlp.py +320 -0
- provide/foundation/observability/openobserve/search.py +222 -0
- provide/foundation/observability/openobserve/streaming.py +235 -0
- provide/foundation/platform/__init__.py +44 -0
- provide/foundation/platform/detection.py +193 -0
- provide/foundation/platform/info.py +157 -0
- provide/foundation/process/__init__.py +39 -0
- provide/foundation/process/async_runner.py +373 -0
- provide/foundation/process/lifecycle.py +406 -0
- provide/foundation/process/runner.py +390 -0
- provide/foundation/setup/__init__.py +101 -0
- provide/foundation/streams/__init__.py +44 -0
- provide/foundation/streams/console.py +57 -0
- provide/foundation/streams/core.py +65 -0
- provide/foundation/streams/file.py +104 -0
- provide/foundation/testing/__init__.py +166 -0
- provide/foundation/testing/cli.py +227 -0
- provide/foundation/testing/crypto.py +163 -0
- provide/foundation/testing/fixtures.py +49 -0
- provide/foundation/testing/hub.py +23 -0
- provide/foundation/testing/logger.py +106 -0
- provide/foundation/testing/streams.py +54 -0
- provide/foundation/tracer/__init__.py +49 -0
- provide/foundation/tracer/context.py +115 -0
- provide/foundation/tracer/otel.py +135 -0
- provide/foundation/tracer/spans.py +174 -0
- provide/foundation/types.py +32 -0
- provide/foundation/utils/__init__.py +97 -0
- provide/foundation/utils/deps.py +195 -0
- provide/foundation/utils/env.py +491 -0
- provide/foundation/utils/formatting.py +483 -0
- provide/foundation/utils/parsing.py +235 -0
- provide/foundation/utils/rate_limiting.py +112 -0
- provide/foundation/utils/streams.py +67 -0
- provide/foundation/utils/timing.py +93 -0
- provide_foundation-0.0.0.dev0.dist-info/METADATA +469 -0
- provide_foundation-0.0.0.dev0.dist-info/RECORD +149 -0
- provide_foundation-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_foundation-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_foundation-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
- 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
|