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