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