provide-foundation 0.0.0.dev2__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 (155) hide show
  1. provide/foundation/__init__.py +20 -20
  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 +90 -91
  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 +4 -4
  13. provide/foundation/cli/commands/logs/__init__.py +2 -2
  14. provide/foundation/cli/commands/logs/generate.py +2 -2
  15. provide/foundation/cli/commands/logs/query.py +3 -3
  16. provide/foundation/cli/commands/logs/send.py +2 -2
  17. provide/foundation/cli/commands/logs/tail.py +2 -2
  18. provide/foundation/cli/decorators.py +0 -1
  19. provide/foundation/cli/testing.py +0 -5
  20. provide/foundation/cli/utils.py +1 -2
  21. provide/foundation/config/__init__.py +19 -19
  22. provide/foundation/config/base.py +2 -2
  23. provide/foundation/config/converters.py +81 -83
  24. provide/foundation/config/defaults.py +1 -1
  25. provide/foundation/config/env.py +2 -1
  26. provide/foundation/config/loader.py +1 -1
  27. provide/foundation/config/sync.py +8 -6
  28. provide/foundation/config/types.py +5 -5
  29. provide/foundation/config/validators.py +4 -4
  30. provide/foundation/console/output.py +7 -7
  31. provide/foundation/context/core.py +19 -17
  32. provide/foundation/crypto/certificates/__init__.py +9 -5
  33. provide/foundation/crypto/certificates/base.py +2 -2
  34. provide/foundation/crypto/certificates/certificate.py +48 -19
  35. provide/foundation/crypto/certificates/factory.py +26 -18
  36. provide/foundation/crypto/certificates/generator.py +24 -23
  37. provide/foundation/crypto/certificates/loader.py +24 -16
  38. provide/foundation/crypto/certificates/operations.py +17 -10
  39. provide/foundation/crypto/certificates/trust.py +21 -21
  40. provide/foundation/env/__init__.py +28 -0
  41. provide/foundation/env/core.py +218 -0
  42. provide/foundation/errors/__init__.py +3 -2
  43. provide/foundation/errors/decorators.py +0 -3
  44. provide/foundation/errors/types.py +0 -1
  45. provide/foundation/eventsets/display.py +13 -14
  46. provide/foundation/eventsets/registry.py +61 -31
  47. provide/foundation/eventsets/resolver.py +50 -46
  48. provide/foundation/eventsets/sets/das.py +8 -8
  49. provide/foundation/eventsets/sets/database.py +14 -14
  50. provide/foundation/eventsets/sets/http.py +21 -21
  51. provide/foundation/eventsets/sets/llm.py +16 -16
  52. provide/foundation/eventsets/sets/task_queue.py +13 -13
  53. provide/foundation/eventsets/types.py +7 -7
  54. provide/foundation/file/directory.py +1 -1
  55. provide/foundation/file/lock.py +2 -3
  56. provide/foundation/hub/components.py +19 -21
  57. provide/foundation/hub/config.py +25 -19
  58. provide/foundation/hub/discovery.py +5 -4
  59. provide/foundation/hub/handlers.py +13 -5
  60. provide/foundation/hub/lifecycle.py +10 -9
  61. provide/foundation/hub/manager.py +3 -0
  62. provide/foundation/hub/processors.py +8 -3
  63. provide/foundation/integrations/__init__.py +1 -1
  64. provide/foundation/integrations/openobserve/client.py +2 -2
  65. provide/foundation/integrations/openobserve/commands.py +9 -9
  66. provide/foundation/integrations/openobserve/config.py +2 -2
  67. provide/foundation/integrations/openobserve/otlp.py +2 -2
  68. provide/foundation/integrations/openobserve/search.py +1 -2
  69. provide/foundation/integrations/openobserve/streaming.py +1 -1
  70. provide/foundation/logger/__init__.py +0 -1
  71. provide/foundation/logger/config/base.py +1 -1
  72. provide/foundation/logger/config/logging.py +19 -19
  73. provide/foundation/logger/config/telemetry.py +11 -13
  74. provide/foundation/logger/factories.py +2 -2
  75. provide/foundation/logger/processors/main.py +12 -10
  76. provide/foundation/logger/ratelimit/limiters.py +4 -4
  77. provide/foundation/logger/ratelimit/processor.py +1 -1
  78. provide/foundation/logger/setup/coordinator.py +38 -24
  79. provide/foundation/logger/setup/processors.py +3 -3
  80. provide/foundation/logger/setup/testing.py +14 -0
  81. provide/foundation/logger/trace.py +5 -5
  82. provide/foundation/metrics/__init__.py +1 -1
  83. provide/foundation/metrics/otel.py +3 -1
  84. provide/foundation/observability/__init__.py +1 -1
  85. provide/foundation/process/__init__.py +1 -1
  86. provide/foundation/process/exit.py +6 -5
  87. provide/foundation/process/lifecycle.py +41 -18
  88. provide/foundation/resilience/__init__.py +6 -5
  89. provide/foundation/resilience/circuit.py +32 -30
  90. provide/foundation/resilience/decorators.py +58 -42
  91. provide/foundation/resilience/fallback.py +55 -40
  92. provide/foundation/resilience/retry.py +67 -65
  93. provide/foundation/serialization/__init__.py +16 -0
  94. provide/foundation/serialization/core.py +70 -0
  95. provide/foundation/streams/config.py +8 -9
  96. provide/foundation/streams/console.py +3 -3
  97. provide/foundation/streams/core.py +2 -2
  98. provide/foundation/streams/file.py +1 -1
  99. provide/foundation/testing/__init__.py +22 -7
  100. provide/foundation/testing/archive/__init__.py +7 -7
  101. provide/foundation/testing/archive/fixtures.py +58 -54
  102. provide/foundation/testing/cli.py +3 -6
  103. provide/foundation/testing/common/__init__.py +13 -13
  104. provide/foundation/testing/common/fixtures.py +27 -30
  105. provide/foundation/testing/file/__init__.py +15 -15
  106. provide/foundation/testing/file/content_fixtures.py +65 -92
  107. provide/foundation/testing/file/directory_fixtures.py +19 -19
  108. provide/foundation/testing/file/fixtures.py +14 -17
  109. provide/foundation/testing/file/special_fixtures.py +34 -42
  110. provide/foundation/testing/logger.py +28 -23
  111. provide/foundation/testing/mocking/__init__.py +21 -21
  112. provide/foundation/testing/mocking/fixtures.py +80 -67
  113. provide/foundation/testing/process/__init__.py +23 -23
  114. provide/foundation/testing/process/async_fixtures.py +89 -80
  115. provide/foundation/testing/process/fixtures.py +11 -13
  116. provide/foundation/testing/process/subprocess_fixtures.py +41 -40
  117. provide/foundation/testing/threading/__init__.py +17 -17
  118. provide/foundation/testing/threading/basic_fixtures.py +21 -17
  119. provide/foundation/testing/threading/data_fixtures.py +18 -16
  120. provide/foundation/testing/threading/execution_fixtures.py +67 -52
  121. provide/foundation/testing/threading/fixtures.py +10 -14
  122. provide/foundation/testing/threading/sync_fixtures.py +21 -18
  123. provide/foundation/testing/time/__init__.py +11 -11
  124. provide/foundation/testing/time/fixtures.py +91 -79
  125. provide/foundation/testing/transport/__init__.py +9 -9
  126. provide/foundation/testing/transport/fixtures.py +54 -54
  127. provide/foundation/time/__init__.py +18 -0
  128. provide/foundation/time/core.py +63 -0
  129. provide/foundation/tools/__init__.py +2 -2
  130. provide/foundation/tools/base.py +68 -67
  131. provide/foundation/tools/cache.py +62 -69
  132. provide/foundation/tools/downloader.py +51 -56
  133. provide/foundation/tools/installer.py +51 -57
  134. provide/foundation/tools/registry.py +38 -45
  135. provide/foundation/tools/resolver.py +70 -68
  136. provide/foundation/tools/verifier.py +39 -50
  137. provide/foundation/tracer/spans.py +1 -13
  138. provide/foundation/transport/__init__.py +26 -33
  139. provide/foundation/transport/base.py +32 -30
  140. provide/foundation/transport/client.py +44 -49
  141. provide/foundation/transport/config.py +11 -13
  142. provide/foundation/transport/errors.py +13 -27
  143. provide/foundation/transport/http.py +69 -55
  144. provide/foundation/transport/middleware.py +86 -81
  145. provide/foundation/transport/registry.py +29 -27
  146. provide/foundation/transport/types.py +6 -6
  147. provide/foundation/utils/deps.py +3 -2
  148. provide/foundation/utils/parsing.py +7 -7
  149. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
  150. provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
  151. provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
  152. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
  153. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
  154. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
  155. {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,6 @@ the base manager class and metadata structures.
7
7
 
8
8
  from abc import ABC, abstractmethod
9
9
  from pathlib import Path
10
- from typing import Any
11
10
 
12
11
  from attrs import define, field
13
12
 
@@ -20,25 +19,25 @@ log = get_logger(__name__)
20
19
 
21
20
  class ToolError(FoundationError):
22
21
  """Base exception for tool-related errors."""
23
-
22
+
24
23
  pass
25
24
 
26
25
 
27
26
  class ToolNotFoundError(ToolError):
28
27
  """Raised when a tool or version cannot be found."""
29
-
28
+
30
29
  pass
31
30
 
32
31
 
33
32
  class ToolInstallError(ToolError):
34
33
  """Raised when tool installation fails."""
35
-
34
+
36
35
  pass
37
36
 
38
37
 
39
38
  class ToolVerificationError(ToolError):
40
39
  """Raised when tool verification fails."""
41
-
40
+
42
41
  pass
43
42
 
44
43
 
@@ -46,7 +45,7 @@ class ToolVerificationError(ToolError):
46
45
  class ToolMetadata:
47
46
  """
48
47
  Metadata about a tool version.
49
-
48
+
50
49
  Attributes:
51
50
  name: Tool name (e.g., "terraform").
52
51
  version: Version string (e.g., "1.5.0").
@@ -61,7 +60,7 @@ class ToolMetadata:
61
60
  dependencies: Other tools this depends on.
62
61
  executable_name: Name of the executable file.
63
62
  """
64
-
63
+
65
64
  name: str
66
65
  version: str
67
66
  platform: str
@@ -79,26 +78,26 @@ class ToolMetadata:
79
78
  class BaseToolManager(ABC):
80
79
  """
81
80
  Abstract base class for tool managers.
82
-
81
+
83
82
  Provides common functionality for downloading, verifying, and installing
84
83
  development tools. Subclasses must implement platform-specific logic.
85
-
84
+
86
85
  Attributes:
87
86
  config: Configuration object.
88
87
  tool_name: Name of the tool being managed.
89
88
  executable_name: Name of the executable file.
90
89
  supported_platforms: List of supported platforms.
91
90
  """
92
-
91
+
93
92
  # Class attributes to be overridden by subclasses
94
93
  tool_name: str = ""
95
94
  executable_name: str = ""
96
95
  supported_platforms: list[str] = ["linux", "darwin", "windows"]
97
-
96
+
98
97
  def __init__(self, config: BaseConfig):
99
98
  """
100
99
  Initialize the tool manager.
101
-
100
+
102
101
  Args:
103
102
  config: Configuration object containing settings.
104
103
  """
@@ -106,149 +105,150 @@ class BaseToolManager(ABC):
106
105
  raise ToolError("Subclass must define tool_name")
107
106
  if not self.executable_name:
108
107
  raise ToolError("Subclass must define executable_name")
109
-
108
+
110
109
  self.config = config
111
-
110
+
112
111
  # Lazy-load components to avoid circular imports
113
112
  self._cache = None
114
113
  self._downloader = None
115
114
  self._verifier = None
116
115
  self._installer = None
117
116
  self._resolver = None
118
-
117
+
119
118
  log.debug(f"Initialized {self.tool_name} manager")
120
-
119
+
121
120
  @property
122
121
  def cache(self):
123
122
  """Get or create cache instance."""
124
123
  if self._cache is None:
125
124
  from provide.foundation.tools.cache import ToolCache
125
+
126
126
  self._cache = ToolCache()
127
127
  return self._cache
128
-
128
+
129
129
  @property
130
130
  def downloader(self):
131
131
  """Get or create downloader instance."""
132
132
  if self._downloader is None:
133
133
  from provide.foundation.tools.downloader import ToolDownloader
134
134
  from provide.foundation.transport import UniversalClient
135
+
135
136
  self._downloader = ToolDownloader(UniversalClient())
136
137
  return self._downloader
137
-
138
+
138
139
  @property
139
140
  def verifier(self):
140
141
  """Get or create verifier instance."""
141
142
  if self._verifier is None:
142
143
  from provide.foundation.tools.verifier import ToolVerifier
144
+
143
145
  self._verifier = ToolVerifier()
144
146
  return self._verifier
145
-
147
+
146
148
  @property
147
149
  def installer(self):
148
150
  """Get or create installer instance."""
149
151
  if self._installer is None:
150
152
  from provide.foundation.tools.installer import ToolInstaller
153
+
151
154
  self._installer = ToolInstaller()
152
155
  return self._installer
153
-
156
+
154
157
  @property
155
158
  def resolver(self):
156
159
  """Get or create version resolver instance."""
157
160
  if self._resolver is None:
158
161
  from provide.foundation.tools.resolver import VersionResolver
162
+
159
163
  self._resolver = VersionResolver()
160
164
  return self._resolver
161
-
165
+
162
166
  @abstractmethod
163
167
  def get_metadata(self, version: str) -> ToolMetadata:
164
168
  """
165
169
  Get metadata for a specific version.
166
-
170
+
167
171
  Args:
168
172
  version: Version string to get metadata for.
169
-
173
+
170
174
  Returns:
171
175
  ToolMetadata object with download URLs and checksums.
172
176
  """
173
177
  pass
174
-
178
+
175
179
  @abstractmethod
176
180
  def get_available_versions(self) -> list[str]:
177
181
  """
178
182
  Get list of available versions from upstream.
179
-
183
+
180
184
  Returns:
181
185
  List of version strings available for download.
182
186
  """
183
187
  pass
184
-
188
+
185
189
  def resolve_version(self, spec: str) -> str:
186
190
  """
187
191
  Resolve a version specification to a concrete version.
188
-
192
+
189
193
  Args:
190
194
  spec: Version specification (e.g., "latest", "~1.5.0").
191
-
195
+
192
196
  Returns:
193
197
  Concrete version string.
194
-
198
+
195
199
  Raises:
196
200
  ToolNotFoundError: If version cannot be resolved.
197
201
  """
198
202
  available = self.get_available_versions()
199
203
  if not available:
200
204
  raise ToolNotFoundError(f"No versions available for {self.tool_name}")
201
-
205
+
202
206
  resolved = self.resolver.resolve(spec, available)
203
207
  if not resolved:
204
208
  raise ToolNotFoundError(
205
209
  f"Cannot resolve version '{spec}' for {self.tool_name}"
206
210
  )
207
-
211
+
208
212
  log.debug(f"Resolved {self.tool_name} version {spec} to {resolved}")
209
213
  return resolved
210
-
214
+
211
215
  def install(self, version: str = "latest", force: bool = False) -> Path:
212
216
  """
213
217
  Install a specific version of the tool.
214
-
218
+
215
219
  Args:
216
220
  version: Version to install (default: "latest").
217
221
  force: Force reinstall even if cached.
218
-
222
+
219
223
  Returns:
220
224
  Path to the installed tool.
221
-
225
+
222
226
  Raises:
223
227
  ToolInstallError: If installation fails.
224
228
  """
225
229
  # Resolve version
226
230
  if version in ["latest", "stable", "dev"] or version.startswith(("~", "^")):
227
231
  version = self.resolve_version(version)
228
-
232
+
229
233
  # Check cache unless forced
230
234
  if not force:
231
235
  if cached_path := self.cache.get(self.tool_name, version):
232
236
  log.info(f"Using cached {self.tool_name} {version}")
233
237
  return cached_path
234
-
238
+
235
239
  log.info(f"Installing {self.tool_name} {version}")
236
-
240
+
237
241
  # Get metadata
238
242
  metadata = self.get_metadata(version)
239
243
  if not metadata.download_url:
240
- raise ToolInstallError(
241
- f"No download URL for {self.tool_name} {version}"
242
- )
243
-
244
+ raise ToolInstallError(f"No download URL for {self.tool_name} {version}")
245
+
244
246
  # Download
245
247
  download_path = Path("/tmp") / f"{self.tool_name}-{version}"
246
248
  artifact_path = self.downloader.download_with_progress(
247
- metadata.download_url,
248
- download_path,
249
- metadata.checksum
249
+ metadata.download_url, download_path, metadata.checksum
250
250
  )
251
-
251
+
252
252
  # Verify if checksum provided
253
253
  if metadata.checksum:
254
254
  if not self.verifier.verify_checksum(artifact_path, metadata.checksum):
@@ -256,79 +256,80 @@ class BaseToolManager(ABC):
256
256
  raise ToolVerificationError(
257
257
  f"Checksum verification failed for {self.tool_name} {version}"
258
258
  )
259
-
259
+
260
260
  # Install
261
261
  install_path = self.installer.install(artifact_path, metadata)
262
-
262
+
263
263
  # Cache the installation
264
264
  self.cache.store(self.tool_name, version, install_path)
265
-
265
+
266
266
  # Clean up download
267
267
  if artifact_path.exists():
268
268
  artifact_path.unlink()
269
-
269
+
270
270
  log.info(f"Successfully installed {self.tool_name} {version} to {install_path}")
271
271
  return install_path
272
-
272
+
273
273
  def uninstall(self, version: str) -> bool:
274
274
  """
275
275
  Uninstall a specific version.
276
-
276
+
277
277
  Args:
278
278
  version: Version to uninstall.
279
-
279
+
280
280
  Returns:
281
281
  True if uninstalled, False if not found.
282
282
  """
283
283
  # Invalidate cache
284
284
  self.cache.invalidate(self.tool_name, version)
285
-
285
+
286
286
  # Remove from filesystem
287
287
  install_path = self.get_install_path(version)
288
288
  if install_path.exists():
289
289
  import shutil
290
+
290
291
  shutil.rmtree(install_path)
291
292
  log.info(f"Uninstalled {self.tool_name} {version}")
292
293
  return True
293
-
294
+
294
295
  return False
295
-
296
+
296
297
  def get_install_path(self, version: str) -> Path:
297
298
  """
298
299
  Get the installation path for a version.
299
-
300
+
300
301
  Args:
301
302
  version: Version string.
302
-
303
+
303
304
  Returns:
304
305
  Path where the version is/will be installed.
305
306
  """
306
307
  base_path = Path.home() / ".wrknv" / "tools" / self.tool_name / version
307
308
  return base_path
308
-
309
+
309
310
  def is_installed(self, version: str) -> bool:
310
311
  """
311
312
  Check if a version is installed.
312
-
313
+
313
314
  Args:
314
315
  version: Version to check.
315
-
316
+
316
317
  Returns:
317
318
  True if installed, False otherwise.
318
319
  """
319
320
  install_path = self.get_install_path(version)
320
321
  executable = install_path / "bin" / self.executable_name
321
322
  return executable.exists()
322
-
323
+
323
324
  def get_platform_info(self) -> dict[str, str]:
324
325
  """
325
326
  Get current platform information.
326
-
327
+
327
328
  Returns:
328
329
  Dictionary with platform and arch keys.
329
330
  """
330
331
  import platform
331
-
332
+
332
333
  system = platform.system().lower()
333
334
  if system == "darwin":
334
335
  system = "darwin"
@@ -336,7 +337,7 @@ class BaseToolManager(ABC):
336
337
  system = "linux"
337
338
  elif system == "windows":
338
339
  system = "windows"
339
-
340
+
340
341
  machine = platform.machine().lower()
341
342
  if machine in ["x86_64", "amd64"]:
342
343
  arch = "amd64"
@@ -344,5 +345,5 @@ class BaseToolManager(ABC):
344
345
  arch = "arm64"
345
346
  else:
346
347
  arch = machine
347
-
348
- return {"platform": system, "arch": arch}
348
+
349
+ return {"platform": system, "arch": arch}
@@ -5,8 +5,8 @@ Provides TTL-based caching to avoid re-downloading tools
5
5
  that are already installed and valid.
6
6
  """
7
7
 
8
- import json
9
8
  from datetime import datetime, timedelta
9
+ import json
10
10
  from pathlib import Path
11
11
 
12
12
  from provide.foundation.errors import FoundationError
@@ -19,35 +19,35 @@ log = get_logger(__name__)
19
19
 
20
20
  class CacheError(FoundationError):
21
21
  """Raised when cache operations fail."""
22
-
22
+
23
23
  pass
24
24
 
25
25
 
26
26
  class ToolCache:
27
27
  """
28
28
  Cache for installed tools with TTL support.
29
-
29
+
30
30
  Tracks installed tool locations and expiration times to
31
31
  avoid unnecessary re-downloads and installations.
32
32
  """
33
-
34
- def __init__(self, cache_dir: Path | None = None):
33
+
34
+ def __init__(self, cache_dir: Path | None = None) -> None:
35
35
  """
36
36
  Initialize the cache.
37
-
37
+
38
38
  Args:
39
39
  cache_dir: Cache directory (defaults to ~/.wrknv/cache).
40
40
  """
41
41
  self.cache_dir = cache_dir or (Path.home() / ".wrknv" / "cache")
42
42
  self.cache_dir.mkdir(parents=True, exist_ok=True)
43
-
43
+
44
44
  self.metadata_file = self.cache_dir / "metadata.json"
45
45
  self.metadata = self._load_metadata()
46
-
46
+
47
47
  def _load_metadata(self) -> dict[str, dict]:
48
48
  """
49
49
  Load cache metadata from disk.
50
-
50
+
51
51
  Returns:
52
52
  Cache metadata dictionary.
53
53
  """
@@ -57,61 +57,55 @@ class ToolCache:
57
57
  return json.loads(content)
58
58
  except Exception as e:
59
59
  log.warning(f"Failed to parse cache metadata: {e}")
60
-
60
+
61
61
  return {}
62
-
62
+
63
63
  def _save_metadata(self) -> None:
64
64
  """Save cache metadata to disk."""
65
65
  try:
66
66
  content = json.dumps(self.metadata, indent=2)
67
- atomic_write(self.metadata_file, content)
67
+ atomic_write(self.metadata_file, content.encode("utf-8"))
68
68
  except Exception as e:
69
69
  log.error(f"Failed to save cache metadata: {e}")
70
-
70
+
71
71
  def get(self, tool: str, version: str) -> Path | None:
72
72
  """
73
73
  Get cached tool path if valid.
74
-
74
+
75
75
  Args:
76
76
  tool: Tool name.
77
77
  version: Tool version.
78
-
78
+
79
79
  Returns:
80
80
  Path to cached tool if valid, None otherwise.
81
81
  """
82
82
  key = f"{tool}:{version}"
83
-
83
+
84
84
  if entry := self.metadata.get(key):
85
85
  path = Path(entry["path"])
86
-
86
+
87
87
  # Check if path exists
88
88
  if not path.exists():
89
89
  log.debug(f"Cache miss: {key} path doesn't exist")
90
90
  self.invalidate(tool, version)
91
91
  return None
92
-
92
+
93
93
  # Check if expired
94
94
  if self._is_expired(entry):
95
95
  log.debug(f"Cache miss: {key} expired")
96
96
  self.invalidate(tool, version)
97
97
  return None
98
-
98
+
99
99
  log.debug(f"Cache hit: {key}")
100
100
  return path
101
-
101
+
102
102
  log.debug(f"Cache miss: {key} not in cache")
103
103
  return None
104
-
105
- def store(
106
- self,
107
- tool: str,
108
- version: str,
109
- path: Path,
110
- ttl_days: int = 7
111
- ) -> None:
104
+
105
+ def store(self, tool: str, version: str, path: Path, ttl_days: int = 7) -> None:
112
106
  """
113
107
  Store tool in cache.
114
-
108
+
115
109
  Args:
116
110
  tool: Tool name.
117
111
  version: Tool version.
@@ -119,7 +113,7 @@ class ToolCache:
119
113
  ttl_days: Time-to-live in days.
120
114
  """
121
115
  key = f"{tool}:{version}"
122
-
116
+
123
117
  self.metadata[key] = {
124
118
  "path": str(path),
125
119
  "tool": tool,
@@ -127,14 +121,14 @@ class ToolCache:
127
121
  "cached_at": datetime.now().isoformat(),
128
122
  "ttl_days": ttl_days,
129
123
  }
130
-
124
+
131
125
  self._save_metadata()
132
126
  log.debug(f"Cached {key} at {path} (TTL: {ttl_days} days)")
133
-
127
+
134
128
  def invalidate(self, tool: str, version: str | None = None) -> None:
135
129
  """
136
130
  Invalidate cache entries.
137
-
131
+
138
132
  Args:
139
133
  tool: Tool name.
140
134
  version: Specific version, or None for all versions.
@@ -148,60 +142,59 @@ class ToolCache:
148
142
  else:
149
143
  # Invalidate all versions of tool
150
144
  keys_to_remove = [
151
- k for k in self.metadata
152
- if self.metadata[k].get("tool") == tool
145
+ k for k in self.metadata if self.metadata[k].get("tool") == tool
153
146
  ]
154
147
  for key in keys_to_remove:
155
148
  del self.metadata[key]
156
149
  log.debug(f"Invalidated cache for {key}")
157
-
150
+
158
151
  self._save_metadata()
159
-
152
+
160
153
  def _is_expired(self, entry: dict) -> bool:
161
154
  """
162
155
  Check if cache entry is expired.
163
-
156
+
164
157
  Args:
165
158
  entry: Cache entry dictionary.
166
-
159
+
167
160
  Returns:
168
161
  True if expired, False otherwise.
169
162
  """
170
163
  try:
171
164
  cached_at = datetime.fromisoformat(entry["cached_at"])
172
165
  ttl_days = entry.get("ttl_days", 7)
173
-
166
+
174
167
  if ttl_days <= 0:
175
168
  # Never expires
176
169
  return False
177
-
170
+
178
171
  expiry = cached_at + timedelta(days=ttl_days)
179
172
  return datetime.now() > expiry
180
173
  except Exception as e:
181
174
  log.debug(f"Error checking expiry: {e}")
182
175
  return True # Treat as expired if we can't determine
183
-
176
+
184
177
  def clear(self) -> None:
185
178
  """Clear all cache entries."""
186
179
  self.metadata = {}
187
180
  self._save_metadata()
188
181
  log.info("Cleared tool cache")
189
-
182
+
190
183
  def list_cached(self) -> list[dict]:
191
184
  """
192
185
  List all cached tools.
193
-
186
+
194
187
  Returns:
195
188
  List of cache entries with metadata.
196
189
  """
197
190
  results = []
198
-
191
+
199
192
  for key, entry in self.metadata.items():
200
193
  # Add expiry status
201
194
  entry = entry.copy()
202
195
  entry["key"] = key
203
196
  entry["expired"] = self._is_expired(entry)
204
-
197
+
205
198
  # Calculate days until expiry
206
199
  try:
207
200
  cached_at = datetime.fromisoformat(entry["cached_at"])
@@ -214,55 +207,55 @@ class ToolCache:
214
207
  entry["days_until_expiry"] = -1 # Never expires
215
208
  except Exception:
216
209
  entry["days_until_expiry"] = 0
217
-
210
+
218
211
  results.append(entry)
219
-
212
+
220
213
  return results
221
-
214
+
222
215
  def get_size(self) -> int:
223
216
  """
224
217
  Get total size of cached tools in bytes.
225
-
218
+
226
219
  Returns:
227
220
  Total size in bytes.
228
221
  """
229
222
  total = 0
230
-
223
+
231
224
  for entry in self.metadata.values():
232
225
  path = Path(entry["path"])
233
- if path.exists():
234
- try:
226
+ try:
227
+ if path.exists():
235
228
  # Calculate directory size
236
229
  if path.is_dir():
237
- total += sum(
238
- f.stat().st_size
239
- for f in path.rglob("*")
240
- if f.is_file()
241
- )
230
+ for f in path.rglob("*"):
231
+ if f.is_file():
232
+ try:
233
+ total += f.stat().st_size
234
+ except Exception as e:
235
+ log.debug(f"Failed to get size of file {f}: {e}")
242
236
  else:
243
237
  total += path.stat().st_size
244
- except Exception as e:
245
- log.debug(f"Failed to get size of {path}: {e}")
246
-
238
+ except Exception as e:
239
+ log.debug(f"Failed to get size of {path}: {e}")
240
+
247
241
  return total
248
-
242
+
249
243
  def prune_expired(self) -> int:
250
244
  """
251
245
  Remove expired entries from cache.
252
-
246
+
253
247
  Returns:
254
248
  Number of entries removed.
255
249
  """
256
250
  expired_keys = [
257
- key for key, entry in self.metadata.items()
258
- if self._is_expired(entry)
251
+ key for key, entry in self.metadata.items() if self._is_expired(entry)
259
252
  ]
260
-
253
+
261
254
  for key in expired_keys:
262
255
  del self.metadata[key]
263
-
256
+
264
257
  if expired_keys:
265
258
  self._save_metadata()
266
259
  log.info(f"Pruned {len(expired_keys)} expired cache entries")
267
-
268
- return len(expired_keys)
260
+
261
+ return len(expired_keys)