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.
Files changed (163) hide show
  1. provide/foundation/__init__.py +36 -10
  2. provide/foundation/archive/__init__.py +1 -1
  3. provide/foundation/archive/base.py +15 -14
  4. provide/foundation/archive/bzip2.py +40 -40
  5. provide/foundation/archive/gzip.py +42 -42
  6. provide/foundation/archive/operations.py +93 -96
  7. provide/foundation/archive/tar.py +33 -31
  8. provide/foundation/archive/zip.py +52 -50
  9. provide/foundation/asynctools/__init__.py +20 -0
  10. provide/foundation/asynctools/core.py +126 -0
  11. provide/foundation/cli/__init__.py +2 -2
  12. provide/foundation/cli/commands/deps.py +15 -9
  13. provide/foundation/cli/commands/logs/__init__.py +3 -3
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +4 -4
  16. provide/foundation/cli/commands/logs/send.py +3 -3
  17. provide/foundation/cli/commands/logs/tail.py +3 -3
  18. provide/foundation/cli/decorators.py +11 -11
  19. provide/foundation/cli/main.py +1 -1
  20. provide/foundation/cli/testing.py +2 -40
  21. provide/foundation/cli/utils.py +21 -18
  22. provide/foundation/config/__init__.py +35 -2
  23. provide/foundation/config/base.py +2 -2
  24. provide/foundation/config/converters.py +477 -0
  25. provide/foundation/config/defaults.py +67 -0
  26. provide/foundation/config/env.py +6 -20
  27. provide/foundation/config/loader.py +10 -4
  28. provide/foundation/config/sync.py +8 -6
  29. provide/foundation/config/types.py +5 -5
  30. provide/foundation/config/validators.py +4 -4
  31. provide/foundation/console/input.py +5 -5
  32. provide/foundation/console/output.py +36 -14
  33. provide/foundation/context/__init__.py +8 -4
  34. provide/foundation/context/core.py +88 -110
  35. provide/foundation/crypto/certificates/__init__.py +9 -5
  36. provide/foundation/crypto/certificates/base.py +2 -2
  37. provide/foundation/crypto/certificates/certificate.py +48 -19
  38. provide/foundation/crypto/certificates/factory.py +26 -18
  39. provide/foundation/crypto/certificates/generator.py +24 -23
  40. provide/foundation/crypto/certificates/loader.py +24 -16
  41. provide/foundation/crypto/certificates/operations.py +17 -10
  42. provide/foundation/crypto/certificates/trust.py +21 -21
  43. provide/foundation/env/__init__.py +28 -0
  44. provide/foundation/env/core.py +218 -0
  45. provide/foundation/errors/__init__.py +3 -3
  46. provide/foundation/errors/decorators.py +0 -234
  47. provide/foundation/errors/types.py +0 -98
  48. provide/foundation/eventsets/display.py +13 -14
  49. provide/foundation/eventsets/registry.py +61 -31
  50. provide/foundation/eventsets/resolver.py +50 -46
  51. provide/foundation/eventsets/sets/das.py +8 -8
  52. provide/foundation/eventsets/sets/database.py +14 -14
  53. provide/foundation/eventsets/sets/http.py +21 -21
  54. provide/foundation/eventsets/sets/llm.py +16 -16
  55. provide/foundation/eventsets/sets/task_queue.py +13 -13
  56. provide/foundation/eventsets/types.py +7 -7
  57. provide/foundation/file/directory.py +14 -23
  58. provide/foundation/file/lock.py +4 -3
  59. provide/foundation/hub/components.py +75 -389
  60. provide/foundation/hub/config.py +157 -0
  61. provide/foundation/hub/discovery.py +63 -0
  62. provide/foundation/hub/handlers.py +89 -0
  63. provide/foundation/hub/lifecycle.py +195 -0
  64. provide/foundation/hub/manager.py +7 -4
  65. provide/foundation/hub/processors.py +49 -0
  66. provide/foundation/integrations/__init__.py +11 -0
  67. provide/foundation/{observability → integrations}/openobserve/__init__.py +10 -7
  68. provide/foundation/{observability → integrations}/openobserve/auth.py +1 -1
  69. provide/foundation/{observability → integrations}/openobserve/client.py +14 -14
  70. provide/foundation/{observability → integrations}/openobserve/commands.py +12 -12
  71. provide/foundation/integrations/openobserve/config.py +37 -0
  72. provide/foundation/{observability → integrations}/openobserve/formatters.py +1 -1
  73. provide/foundation/{observability → integrations}/openobserve/otlp.py +2 -2
  74. provide/foundation/{observability → integrations}/openobserve/search.py +2 -3
  75. provide/foundation/{observability → integrations}/openobserve/streaming.py +5 -5
  76. provide/foundation/logger/__init__.py +0 -1
  77. provide/foundation/logger/config/base.py +1 -1
  78. provide/foundation/logger/config/logging.py +69 -299
  79. provide/foundation/logger/config/telemetry.py +39 -121
  80. provide/foundation/logger/factories.py +2 -2
  81. provide/foundation/logger/processors/main.py +12 -10
  82. provide/foundation/logger/ratelimit/limiters.py +4 -4
  83. provide/foundation/logger/ratelimit/processor.py +1 -1
  84. provide/foundation/logger/setup/coordinator.py +39 -25
  85. provide/foundation/logger/setup/processors.py +3 -3
  86. provide/foundation/logger/setup/testing.py +14 -0
  87. provide/foundation/logger/trace.py +5 -5
  88. provide/foundation/metrics/__init__.py +1 -1
  89. provide/foundation/metrics/otel.py +3 -1
  90. provide/foundation/observability/__init__.py +3 -3
  91. provide/foundation/process/__init__.py +9 -0
  92. provide/foundation/process/exit.py +48 -0
  93. provide/foundation/process/lifecycle.py +69 -46
  94. provide/foundation/resilience/__init__.py +36 -0
  95. provide/foundation/resilience/circuit.py +166 -0
  96. provide/foundation/resilience/decorators.py +236 -0
  97. provide/foundation/resilience/fallback.py +208 -0
  98. provide/foundation/resilience/retry.py +327 -0
  99. provide/foundation/serialization/__init__.py +16 -0
  100. provide/foundation/serialization/core.py +70 -0
  101. provide/foundation/streams/config.py +78 -0
  102. provide/foundation/streams/console.py +4 -5
  103. provide/foundation/streams/core.py +5 -2
  104. provide/foundation/streams/file.py +12 -2
  105. provide/foundation/testing/__init__.py +29 -9
  106. provide/foundation/testing/archive/__init__.py +7 -7
  107. provide/foundation/testing/archive/fixtures.py +58 -54
  108. provide/foundation/testing/cli.py +30 -20
  109. provide/foundation/testing/common/__init__.py +13 -15
  110. provide/foundation/testing/common/fixtures.py +27 -57
  111. provide/foundation/testing/file/__init__.py +15 -15
  112. provide/foundation/testing/file/content_fixtures.py +289 -0
  113. provide/foundation/testing/file/directory_fixtures.py +107 -0
  114. provide/foundation/testing/file/fixtures.py +42 -516
  115. provide/foundation/testing/file/special_fixtures.py +145 -0
  116. provide/foundation/testing/logger.py +89 -8
  117. provide/foundation/testing/mocking/__init__.py +21 -21
  118. provide/foundation/testing/mocking/fixtures.py +80 -67
  119. provide/foundation/testing/process/__init__.py +23 -23
  120. provide/foundation/testing/process/async_fixtures.py +414 -0
  121. provide/foundation/testing/process/fixtures.py +48 -571
  122. provide/foundation/testing/process/subprocess_fixtures.py +210 -0
  123. provide/foundation/testing/threading/__init__.py +17 -17
  124. provide/foundation/testing/threading/basic_fixtures.py +105 -0
  125. provide/foundation/testing/threading/data_fixtures.py +101 -0
  126. provide/foundation/testing/threading/execution_fixtures.py +278 -0
  127. provide/foundation/testing/threading/fixtures.py +32 -502
  128. provide/foundation/testing/threading/sync_fixtures.py +100 -0
  129. provide/foundation/testing/time/__init__.py +11 -11
  130. provide/foundation/testing/time/fixtures.py +95 -83
  131. provide/foundation/testing/transport/__init__.py +9 -9
  132. provide/foundation/testing/transport/fixtures.py +54 -54
  133. provide/foundation/time/__init__.py +18 -0
  134. provide/foundation/time/core.py +63 -0
  135. provide/foundation/tools/__init__.py +2 -2
  136. provide/foundation/tools/base.py +68 -67
  137. provide/foundation/tools/cache.py +69 -74
  138. provide/foundation/tools/downloader.py +68 -62
  139. provide/foundation/tools/installer.py +51 -57
  140. provide/foundation/tools/registry.py +38 -45
  141. provide/foundation/tools/resolver.py +70 -68
  142. provide/foundation/tools/verifier.py +39 -50
  143. provide/foundation/tracer/spans.py +2 -14
  144. provide/foundation/transport/__init__.py +26 -33
  145. provide/foundation/transport/base.py +32 -30
  146. provide/foundation/transport/client.py +44 -49
  147. provide/foundation/transport/config.py +36 -107
  148. provide/foundation/transport/errors.py +13 -27
  149. provide/foundation/transport/http.py +69 -55
  150. provide/foundation/transport/middleware.py +113 -114
  151. provide/foundation/transport/registry.py +29 -27
  152. provide/foundation/transport/types.py +6 -6
  153. provide/foundation/utils/deps.py +17 -14
  154. provide/foundation/utils/parsing.py +49 -4
  155. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  156. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  157. provide_foundation-0.0.0.dev1.dist-info/RECORD +0 -200
  158. /provide/foundation/{observability → integrations}/openobserve/exceptions.py +0 -0
  159. /provide/foundation/{observability → integrations}/openobserve/models.py +0 -0
  160. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  161. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  162. {provide_foundation-0.0.0.dev1.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  163. {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(versions, key=lambda v: (
319
- self.parse_version(v),
320
- v # Secondary sort by string for pre-releases
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, Optional
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: Optional["otel_trace.Span"] = field(
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()