provide-foundation 0.0.0.dev1__py3-none-any.whl → 0.0.0.dev3__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/foundation/__init__.py +36 -10
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +93 -96
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +15 -9
- provide/foundation/cli/commands/logs/__init__.py +3 -3
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +4 -4
- provide/foundation/cli/commands/logs/send.py +3 -3
- provide/foundation/cli/commands/logs/tail.py +3 -3
- provide/foundation/cli/decorators.py +11 -11
- provide/foundation/cli/main.py +1 -1
- provide/foundation/cli/testing.py +2 -40
- provide/foundation/cli/utils.py +21 -18
- provide/foundation/config/__init__.py +35 -2
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +477 -0
- provide/foundation/config/defaults.py +67 -0
- provide/foundation/config/env.py +6 -20
- provide/foundation/config/loader.py +10 -4
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/input.py +5 -5
- provide/foundation/console/output.py +36 -14
- provide/foundation/context/__init__.py +8 -4
- provide/foundation/context/core.py +88 -110
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -3
- provide/foundation/errors/decorators.py +0 -234
- provide/foundation/errors/types.py +0 -98
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +14 -23
- provide/foundation/file/lock.py +4 -3
- provide/foundation/hub/components.py +75 -389
- provide/foundation/hub/config.py +157 -0
- provide/foundation/hub/discovery.py +63 -0
- provide/foundation/hub/handlers.py +89 -0
- provide/foundation/hub/lifecycle.py +195 -0
- provide/foundation/hub/manager.py +7 -4
- provide/foundation/hub/processors.py +49 -0
- provide/foundation/integrations/__init__.py +11 -0
- provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
- provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
- provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
- provide/foundation/integrations/openobserve/config.py +37 -0
- provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
- provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
- provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
- provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +69 -299
- provide/foundation/logger/config/telemetry.py +39 -121
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +39 -25
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +3 -3
- provide/foundation/process/__init__.py +9 -0
- provide/foundation/process/exit.py +48 -0
- provide/foundation/process/lifecycle.py +69 -46
- provide/foundation/resilience/__init__.py +36 -0
- provide/foundation/resilience/circuit.py +166 -0
- provide/foundation/resilience/decorators.py +236 -0
- provide/foundation/resilience/fallback.py +208 -0
- provide/foundation/resilience/retry.py +327 -0
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +78 -0
- provide/foundation/streams/console.py +4 -5
- provide/foundation/streams/core.py +5 -2
- provide/foundation/streams/file.py +12 -2
- provide/foundation/testing/__init__.py +29 -9
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +30 -20
- provide/foundation/testing/common/__init__.py +13 -15
- provide/foundation/testing/common/fixtures.py +27 -57
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +289 -0
- provide/foundation/testing/file/directory_fixtures.py +107 -0
- provide/foundation/testing/file/fixtures.py +42 -516
- provide/foundation/testing/file/special_fixtures.py +145 -0
- provide/foundation/testing/logger.py +89 -8
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +414 -0
- provide/foundation/testing/process/fixtures.py +48 -571
- provide/foundation/testing/process/subprocess_fixtures.py +210 -0
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +105 -0
- provide/foundation/testing/threading/data_fixtures.py +101 -0
- provide/foundation/testing/threading/execution_fixtures.py +278 -0
- provide/foundation/testing/threading/fixtures.py +32 -502
- provide/foundation/testing/threading/sync_fixtures.py +100 -0
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +95 -83
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +69 -74
- provide/foundation/tools/downloader.py +68 -62
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +2 -14
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +36 -107
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +113 -114
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +17 -14
- provide/foundation/utils/parsing.py +49 -4
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
- /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
- /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ semver ranges, wildcards, and pre-release handling.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import re
|
9
|
-
from typing import Any
|
10
9
|
|
11
10
|
from provide.foundation.errors import FoundationError
|
12
11
|
from provide.foundation.logger import get_logger
|
@@ -16,14 +15,14 @@ log = get_logger(__name__)
|
|
16
15
|
|
17
16
|
class ResolutionError(FoundationError):
|
18
17
|
"""Raised when version resolution fails."""
|
19
|
-
|
18
|
+
|
20
19
|
pass
|
21
20
|
|
22
21
|
|
23
22
|
class VersionResolver:
|
24
23
|
"""
|
25
24
|
Resolve version specifications to concrete versions.
|
26
|
-
|
25
|
+
|
27
26
|
Supports:
|
28
27
|
- "latest": Most recent stable version
|
29
28
|
- "latest-beta": Most recent pre-release
|
@@ -32,23 +31,23 @@ class VersionResolver:
|
|
32
31
|
- "1.2.*": Wildcard matching
|
33
32
|
- Exact versions
|
34
33
|
"""
|
35
|
-
|
34
|
+
|
36
35
|
def resolve(self, spec: str, available: list[str]) -> str | None:
|
37
36
|
"""
|
38
37
|
Resolve a version specification to a concrete version.
|
39
|
-
|
38
|
+
|
40
39
|
Args:
|
41
40
|
spec: Version specification.
|
42
41
|
available: List of available versions.
|
43
|
-
|
42
|
+
|
44
43
|
Returns:
|
45
44
|
Resolved version string, or None if not found.
|
46
45
|
"""
|
47
46
|
if not available:
|
48
47
|
return None
|
49
|
-
|
48
|
+
|
50
49
|
spec = spec.strip()
|
51
|
-
|
50
|
+
|
52
51
|
# Handle special keywords
|
53
52
|
if spec == "latest":
|
54
53
|
return self.get_latest_stable(available)
|
@@ -56,77 +55,77 @@ class VersionResolver:
|
|
56
55
|
return self.get_latest_prerelease(available)
|
57
56
|
elif spec == "latest-any":
|
58
57
|
return self.get_latest_any(available)
|
59
|
-
|
58
|
+
|
60
59
|
# Handle ranges
|
61
60
|
elif spec.startswith("~"):
|
62
61
|
return self.resolve_tilde(spec[1:], available)
|
63
62
|
elif spec.startswith("^"):
|
64
63
|
return self.resolve_caret(spec[1:], available)
|
65
|
-
|
64
|
+
|
66
65
|
# Handle wildcards
|
67
66
|
elif "*" in spec:
|
68
67
|
return self.resolve_wildcard(spec, available)
|
69
|
-
|
68
|
+
|
70
69
|
# Exact match
|
71
70
|
elif spec in available:
|
72
71
|
return spec
|
73
|
-
|
72
|
+
|
74
73
|
return None
|
75
|
-
|
74
|
+
|
76
75
|
def get_latest_stable(self, versions: list[str]) -> str | None:
|
77
76
|
"""
|
78
77
|
Get latest stable version (no pre-release).
|
79
|
-
|
78
|
+
|
80
79
|
Args:
|
81
80
|
versions: List of available versions.
|
82
|
-
|
81
|
+
|
83
82
|
Returns:
|
84
83
|
Latest stable version, or None if no stable versions.
|
85
84
|
"""
|
86
85
|
stable = [v for v in versions if not self.is_prerelease(v)]
|
87
86
|
if not stable:
|
88
87
|
return None
|
89
|
-
|
88
|
+
|
90
89
|
return self.sort_versions(stable)[-1]
|
91
|
-
|
90
|
+
|
92
91
|
def get_latest_prerelease(self, versions: list[str]) -> str | None:
|
93
92
|
"""
|
94
93
|
Get latest pre-release version.
|
95
|
-
|
94
|
+
|
96
95
|
Args:
|
97
96
|
versions: List of available versions.
|
98
|
-
|
97
|
+
|
99
98
|
Returns:
|
100
99
|
Latest pre-release version, or None if no pre-releases.
|
101
100
|
"""
|
102
101
|
prerelease = [v for v in versions if self.is_prerelease(v)]
|
103
102
|
if not prerelease:
|
104
103
|
return None
|
105
|
-
|
104
|
+
|
106
105
|
return self.sort_versions(prerelease)[-1]
|
107
|
-
|
106
|
+
|
108
107
|
def get_latest_any(self, versions: list[str]) -> str | None:
|
109
108
|
"""
|
110
109
|
Get latest version (including pre-releases).
|
111
|
-
|
110
|
+
|
112
111
|
Args:
|
113
112
|
versions: List of available versions.
|
114
|
-
|
113
|
+
|
115
114
|
Returns:
|
116
115
|
Latest version, or None if list is empty.
|
117
116
|
"""
|
118
117
|
if not versions:
|
119
118
|
return None
|
120
|
-
|
119
|
+
|
121
120
|
return self.sort_versions(versions)[-1]
|
122
|
-
|
121
|
+
|
123
122
|
def is_prerelease(self, version: str) -> bool:
|
124
123
|
"""
|
125
124
|
Check if version is a pre-release.
|
126
|
-
|
125
|
+
|
127
126
|
Args:
|
128
127
|
version: Version string.
|
129
|
-
|
128
|
+
|
130
129
|
Returns:
|
131
130
|
True if version appears to be pre-release.
|
132
131
|
"""
|
@@ -144,22 +143,22 @@ class VersionResolver:
|
|
144
143
|
r"b\d+$", # 1.0b2
|
145
144
|
r"rc\d+$", # 1.0rc3
|
146
145
|
]
|
147
|
-
|
146
|
+
|
148
147
|
version_lower = version.lower()
|
149
148
|
for pattern in prerelease_patterns:
|
150
149
|
if re.search(pattern, version_lower):
|
151
150
|
return True
|
152
|
-
|
151
|
+
|
153
152
|
return False
|
154
|
-
|
153
|
+
|
155
154
|
def resolve_tilde(self, base: str, available: list[str]) -> str | None:
|
156
155
|
"""
|
157
156
|
Resolve tilde range (~1.2.3 means >=1.2.3 <1.3.0).
|
158
|
-
|
157
|
+
|
159
158
|
Args:
|
160
159
|
base: Base version without tilde.
|
161
160
|
available: List of available versions.
|
162
|
-
|
161
|
+
|
163
162
|
Returns:
|
164
163
|
Best matching version, or None if no match.
|
165
164
|
"""
|
@@ -167,9 +166,9 @@ class VersionResolver:
|
|
167
166
|
parts = self.parse_version(base)
|
168
167
|
if len(parts) < 2:
|
169
168
|
return None
|
170
|
-
|
169
|
+
|
171
170
|
major, minor = parts[0], parts[1]
|
172
|
-
|
171
|
+
|
173
172
|
# Filter versions that match the constraint
|
174
173
|
matches = []
|
175
174
|
for v in available:
|
@@ -182,22 +181,22 @@ class VersionResolver:
|
|
182
181
|
matches.append(v)
|
183
182
|
else:
|
184
183
|
matches.append(v)
|
185
|
-
|
184
|
+
|
186
185
|
if matches:
|
187
186
|
return self.sort_versions(matches)[-1]
|
188
187
|
except Exception as e:
|
189
188
|
log.debug(f"Failed to resolve tilde range {base}: {e}")
|
190
|
-
|
189
|
+
|
191
190
|
return None
|
192
|
-
|
191
|
+
|
193
192
|
def resolve_caret(self, base: str, available: list[str]) -> str | None:
|
194
193
|
"""
|
195
194
|
Resolve caret range (^1.2.3 means >=1.2.3 <2.0.0).
|
196
|
-
|
195
|
+
|
197
196
|
Args:
|
198
197
|
base: Base version without caret.
|
199
198
|
available: List of available versions.
|
200
|
-
|
199
|
+
|
201
200
|
Returns:
|
202
201
|
Best matching version, or None if no match.
|
203
202
|
"""
|
@@ -205,9 +204,9 @@ class VersionResolver:
|
|
205
204
|
parts = self.parse_version(base)
|
206
205
|
if not parts:
|
207
206
|
return None
|
208
|
-
|
207
|
+
|
209
208
|
major = parts[0]
|
210
|
-
|
209
|
+
|
211
210
|
# Filter versions that match the constraint
|
212
211
|
matches = []
|
213
212
|
for v in available:
|
@@ -216,22 +215,22 @@ class VersionResolver:
|
|
216
215
|
# Must be >= base version
|
217
216
|
if self.compare_versions(v, base) >= 0:
|
218
217
|
matches.append(v)
|
219
|
-
|
218
|
+
|
220
219
|
if matches:
|
221
220
|
return self.sort_versions(matches)[-1]
|
222
221
|
except Exception as e:
|
223
222
|
log.debug(f"Failed to resolve caret range {base}: {e}")
|
224
|
-
|
223
|
+
|
225
224
|
return None
|
226
|
-
|
225
|
+
|
227
226
|
def resolve_wildcard(self, pattern: str, available: list[str]) -> str | None:
|
228
227
|
"""
|
229
228
|
Resolve wildcard pattern (1.2.* matches any 1.2.x).
|
230
|
-
|
229
|
+
|
231
230
|
Args:
|
232
231
|
pattern: Version pattern with wildcards.
|
233
232
|
available: List of available versions.
|
234
|
-
|
233
|
+
|
235
234
|
Returns:
|
236
235
|
Best matching version, or None if no match.
|
237
236
|
"""
|
@@ -239,26 +238,26 @@ class VersionResolver:
|
|
239
238
|
regex_pattern = pattern.replace(".", r"\.")
|
240
239
|
regex_pattern = regex_pattern.replace("*", r".*")
|
241
240
|
regex_pattern = f"^{regex_pattern}$"
|
242
|
-
|
241
|
+
|
243
242
|
try:
|
244
243
|
regex = re.compile(regex_pattern)
|
245
244
|
matches = [v for v in available if regex.match(v)]
|
246
|
-
|
245
|
+
|
247
246
|
if matches:
|
248
247
|
# Return latest matching version
|
249
248
|
return self.sort_versions(matches)[-1]
|
250
249
|
except Exception as e:
|
251
250
|
log.debug(f"Failed to resolve wildcard {pattern}: {e}")
|
252
|
-
|
251
|
+
|
253
252
|
return None
|
254
|
-
|
253
|
+
|
255
254
|
def parse_version(self, version: str) -> list[int]:
|
256
255
|
"""
|
257
256
|
Parse version string into numeric components.
|
258
|
-
|
257
|
+
|
259
258
|
Args:
|
260
259
|
version: Version string.
|
261
|
-
|
260
|
+
|
262
261
|
Returns:
|
263
262
|
List of numeric version components.
|
264
263
|
"""
|
@@ -266,56 +265,59 @@ class VersionResolver:
|
|
266
265
|
match = re.match(r"^v?(\d+(?:\.\d+)*)", version)
|
267
266
|
if not match:
|
268
267
|
return []
|
269
|
-
|
268
|
+
|
270
269
|
version_str = match.group(1)
|
271
270
|
parts = []
|
272
|
-
|
271
|
+
|
273
272
|
for part in version_str.split("."):
|
274
273
|
try:
|
275
274
|
parts.append(int(part))
|
276
275
|
except ValueError:
|
277
276
|
break
|
278
|
-
|
277
|
+
|
279
278
|
return parts
|
280
|
-
|
279
|
+
|
281
280
|
def compare_versions(self, v1: str, v2: str) -> int:
|
282
281
|
"""
|
283
282
|
Compare two versions.
|
284
|
-
|
283
|
+
|
285
284
|
Args:
|
286
285
|
v1: First version.
|
287
286
|
v2: Second version.
|
288
|
-
|
287
|
+
|
289
288
|
Returns:
|
290
289
|
-1 if v1 < v2, 0 if equal, 1 if v1 > v2.
|
291
290
|
"""
|
292
291
|
parts1 = self.parse_version(v1)
|
293
292
|
parts2 = self.parse_version(v2)
|
294
|
-
|
293
|
+
|
295
294
|
# Pad with zeros
|
296
295
|
max_len = max(len(parts1), len(parts2))
|
297
296
|
parts1.extend([0] * (max_len - len(parts1)))
|
298
297
|
parts2.extend([0] * (max_len - len(parts2)))
|
299
|
-
|
298
|
+
|
300
299
|
for p1, p2 in zip(parts1, parts2):
|
301
300
|
if p1 < p2:
|
302
301
|
return -1
|
303
302
|
elif p1 > p2:
|
304
303
|
return 1
|
305
|
-
|
304
|
+
|
306
305
|
return 0
|
307
|
-
|
306
|
+
|
308
307
|
def sort_versions(self, versions: list[str]) -> list[str]:
|
309
308
|
"""
|
310
309
|
Sort versions in ascending order.
|
311
|
-
|
310
|
+
|
312
311
|
Args:
|
313
312
|
versions: List of version strings.
|
314
|
-
|
313
|
+
|
315
314
|
Returns:
|
316
315
|
Sorted list of versions.
|
317
316
|
"""
|
318
|
-
return sorted(
|
319
|
-
|
320
|
-
v
|
321
|
-
|
317
|
+
return sorted(
|
318
|
+
versions,
|
319
|
+
key=lambda v: (
|
320
|
+
self.parse_version(v),
|
321
|
+
v, # Secondary sort by string for pre-releases
|
322
|
+
),
|
323
|
+
)
|
@@ -6,7 +6,6 @@ checksum algorithms and GPG/PGP signatures.
|
|
6
6
|
"""
|
7
7
|
|
8
8
|
import hashlib
|
9
|
-
import re
|
10
9
|
from pathlib import Path
|
11
10
|
from typing import Literal
|
12
11
|
|
@@ -18,7 +17,7 @@ log = get_logger(__name__)
|
|
18
17
|
|
19
18
|
class VerificationError(FoundationError):
|
20
19
|
"""Raised when verification fails."""
|
21
|
-
|
20
|
+
|
22
21
|
pass
|
23
22
|
|
24
23
|
|
@@ -28,125 +27,115 @@ HashAlgo = Literal["sha256", "sha512", "md5", "blake2b"]
|
|
28
27
|
class ToolVerifier:
|
29
28
|
"""
|
30
29
|
Verify tool artifacts using checksums and signatures.
|
31
|
-
|
30
|
+
|
32
31
|
Supports multiple checksum algorithms and GPG/PGP signatures
|
33
32
|
for ensuring artifact integrity and authenticity.
|
34
33
|
"""
|
35
|
-
|
34
|
+
|
36
35
|
SUPPORTED_ALGORITHMS = ["sha256", "sha512", "md5", "blake2b"]
|
37
36
|
CHUNK_SIZE = 8192 # Read files in 8KB chunks
|
38
|
-
|
37
|
+
|
39
38
|
def verify_checksum(
|
40
|
-
self,
|
41
|
-
file_path: Path,
|
42
|
-
expected: str,
|
43
|
-
algo: HashAlgo = "sha256"
|
39
|
+
self, file_path: Path, expected: str, algo: HashAlgo = "sha256"
|
44
40
|
) -> bool:
|
45
41
|
"""
|
46
42
|
Verify file checksum.
|
47
|
-
|
43
|
+
|
48
44
|
Args:
|
49
45
|
file_path: Path to file to verify.
|
50
46
|
expected: Expected checksum (hex string).
|
51
47
|
algo: Hash algorithm to use.
|
52
|
-
|
48
|
+
|
53
49
|
Returns:
|
54
50
|
True if checksum matches, False otherwise.
|
55
|
-
|
51
|
+
|
56
52
|
Raises:
|
57
53
|
ValueError: If algorithm is not supported.
|
58
54
|
FileNotFoundError: If file doesn't exist.
|
59
55
|
"""
|
60
56
|
if algo not in self.SUPPORTED_ALGORITHMS:
|
61
57
|
raise ValueError(f"Unsupported hash algorithm: {algo}")
|
62
|
-
|
58
|
+
|
63
59
|
if not file_path.exists():
|
64
60
|
raise FileNotFoundError(f"File not found: {file_path}")
|
65
|
-
|
61
|
+
|
66
62
|
log.debug(f"Verifying {algo} checksum for {file_path}")
|
67
|
-
|
63
|
+
|
68
64
|
# Create hasher
|
69
65
|
hasher = hashlib.new(algo)
|
70
|
-
|
66
|
+
|
71
67
|
# Read file in chunks
|
72
68
|
with file_path.open("rb") as f:
|
73
69
|
while chunk := f.read(self.CHUNK_SIZE):
|
74
70
|
hasher.update(chunk)
|
75
|
-
|
71
|
+
|
76
72
|
actual = hasher.hexdigest()
|
77
73
|
matches = actual == expected
|
78
|
-
|
74
|
+
|
79
75
|
if not matches:
|
80
76
|
log.warning(
|
81
77
|
f"Checksum mismatch for {file_path.name}: "
|
82
78
|
f"expected {expected}, got {actual}"
|
83
79
|
)
|
84
|
-
|
80
|
+
|
85
81
|
return matches
|
86
|
-
|
87
|
-
def verify_shasums_file(
|
88
|
-
self,
|
89
|
-
shasums_file: Path,
|
90
|
-
target_file: Path
|
91
|
-
) -> bool:
|
82
|
+
|
83
|
+
def verify_shasums_file(self, shasums_file: Path, target_file: Path) -> bool:
|
92
84
|
"""
|
93
85
|
Verify using a shasums file (common for Go/Terraform).
|
94
|
-
|
86
|
+
|
95
87
|
Args:
|
96
88
|
shasums_file: Path to shasums file.
|
97
89
|
target_file: Path to file to verify.
|
98
|
-
|
90
|
+
|
99
91
|
Returns:
|
100
92
|
True if file is listed and checksum matches, False otherwise.
|
101
93
|
"""
|
102
94
|
log.debug(f"Verifying {target_file.name} using {shasums_file}")
|
103
|
-
|
95
|
+
|
104
96
|
with shasums_file.open() as f:
|
105
97
|
for line in f:
|
106
98
|
line = line.strip()
|
107
99
|
if not line:
|
108
100
|
continue
|
109
|
-
|
101
|
+
|
110
102
|
# Parse line: "checksum filename" or "checksum *filename"
|
111
103
|
parts = line.split(None, 1)
|
112
104
|
if len(parts) != 2:
|
113
105
|
continue
|
114
|
-
|
106
|
+
|
115
107
|
checksum, filename = parts
|
116
108
|
# Remove asterisk prefix if present (binary mode indicator)
|
117
109
|
filename = filename.lstrip("*")
|
118
|
-
|
110
|
+
|
119
111
|
# Check if this is our file
|
120
112
|
if filename == target_file.name:
|
121
113
|
return self.verify_checksum(target_file, checksum)
|
122
|
-
|
114
|
+
|
123
115
|
# File not found in shasums
|
124
116
|
log.warning(f"{target_file.name} not found in {shasums_file}")
|
125
117
|
return False
|
126
|
-
|
118
|
+
|
127
119
|
def verify_signature(
|
128
|
-
self,
|
129
|
-
file_path: Path,
|
130
|
-
signature: str,
|
131
|
-
public_key: str | None = None
|
120
|
+
self, file_path: Path, signature: str, public_key: str | None = None
|
132
121
|
) -> bool:
|
133
122
|
"""
|
134
123
|
Verify GPG/PGP signature.
|
135
|
-
|
124
|
+
|
136
125
|
Args:
|
137
126
|
file_path: Path to file to verify.
|
138
127
|
signature: Signature data.
|
139
128
|
public_key: Optional public key for verification.
|
140
|
-
|
129
|
+
|
141
130
|
Returns:
|
142
131
|
True if signature is valid, False otherwise.
|
143
132
|
"""
|
144
133
|
log.debug(f"Verifying signature for {file_path}")
|
145
|
-
|
134
|
+
|
146
135
|
try:
|
147
136
|
# Use foundation's crypto module
|
148
137
|
from provide.foundation.crypto import verify_signature
|
149
|
-
|
138
|
+
|
150
139
|
return verify_signature(file_path, signature, public_key)
|
151
140
|
except ImportError:
|
152
141
|
log.warning("Crypto module not available, skipping signature verification")
|
@@ -154,33 +143,33 @@ class ToolVerifier:
|
|
154
143
|
except Exception as e:
|
155
144
|
log.error(f"Signature verification failed: {e}")
|
156
145
|
return False
|
157
|
-
|
146
|
+
|
158
147
|
def extract_checksum(self, checksum_string: str) -> str:
|
159
148
|
"""
|
160
149
|
Extract checksum from various string formats.
|
161
|
-
|
150
|
+
|
162
151
|
Handles formats like:
|
163
152
|
- "abc123"
|
164
153
|
- "abc123 filename.tar.gz"
|
165
154
|
- "sha256:abc123"
|
166
155
|
- "SHA256:def456"
|
167
|
-
|
156
|
+
|
168
157
|
Args:
|
169
158
|
checksum_string: String containing checksum.
|
170
|
-
|
159
|
+
|
171
160
|
Returns:
|
172
161
|
Extracted checksum hex string.
|
173
162
|
"""
|
174
163
|
checksum_string = checksum_string.strip()
|
175
|
-
|
164
|
+
|
176
165
|
# Remove algorithm prefix if present
|
177
166
|
if ":" in checksum_string:
|
178
167
|
checksum_string = checksum_string.split(":", 1)[1]
|
179
|
-
|
168
|
+
|
180
169
|
# Take first word (checksum is before any whitespace)
|
181
170
|
checksum = checksum_string.split()[0]
|
182
|
-
|
171
|
+
|
183
172
|
# Remove any asterisk prefix (binary mode indicator)
|
184
173
|
checksum = checksum.lstrip("*")
|
185
|
-
|
186
|
-
return checksum
|
174
|
+
|
175
|
+
return checksum
|
@@ -8,7 +8,7 @@ Provides OpenTelemetry integration when available, falls back to simple tracing.
|
|
8
8
|
|
9
9
|
from dataclasses import dataclass, field
|
10
10
|
import time
|
11
|
-
from typing import Any
|
11
|
+
from typing import Any
|
12
12
|
import uuid
|
13
13
|
|
14
14
|
from provide.foundation.logger import get_logger
|
@@ -47,9 +47,7 @@ class Span:
|
|
47
47
|
error: str | None = None
|
48
48
|
|
49
49
|
# Internal OpenTelemetry span (when available)
|
50
|
-
_otel_span:
|
51
|
-
default=None, init=False, repr=False
|
52
|
-
)
|
50
|
+
_otel_span: "otel_trace.Span | None" = field(default=None, init=False, repr=False)
|
53
51
|
_active: bool = field(default=True, init=False, repr=False)
|
54
52
|
|
55
53
|
def __post_init__(self) -> None:
|
@@ -162,13 +160,3 @@ class Span:
|
|
162
160
|
"status": self.status,
|
163
161
|
"error": self.error,
|
164
162
|
}
|
165
|
-
|
166
|
-
def __enter__(self):
|
167
|
-
"""Context manager entry."""
|
168
|
-
return self
|
169
|
-
|
170
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
171
|
-
"""Context manager exit."""
|
172
|
-
if exc_type is not None:
|
173
|
-
self.set_error(f"{exc_type.__name__}: {exc_val}")
|
174
|
-
self.finish()
|