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.
- provide/foundation/__init__.py +20 -20
- provide/foundation/archive/__init__.py +1 -1
- provide/foundation/archive/base.py +15 -14
- provide/foundation/archive/bzip2.py +40 -40
- provide/foundation/archive/gzip.py +42 -42
- provide/foundation/archive/operations.py +90 -91
- provide/foundation/archive/tar.py +33 -31
- provide/foundation/archive/zip.py +52 -50
- provide/foundation/asynctools/__init__.py +20 -0
- provide/foundation/asynctools/core.py +126 -0
- provide/foundation/cli/__init__.py +2 -2
- provide/foundation/cli/commands/deps.py +4 -4
- provide/foundation/cli/commands/logs/__init__.py +2 -2
- provide/foundation/cli/commands/logs/generate.py +2 -2
- provide/foundation/cli/commands/logs/query.py +3 -3
- provide/foundation/cli/commands/logs/send.py +2 -2
- provide/foundation/cli/commands/logs/tail.py +2 -2
- provide/foundation/cli/decorators.py +0 -1
- provide/foundation/cli/testing.py +0 -5
- provide/foundation/cli/utils.py +1 -2
- provide/foundation/config/__init__.py +19 -19
- provide/foundation/config/base.py +2 -2
- provide/foundation/config/converters.py +81 -83
- provide/foundation/config/defaults.py +1 -1
- provide/foundation/config/env.py +2 -1
- provide/foundation/config/loader.py +1 -1
- provide/foundation/config/sync.py +8 -6
- provide/foundation/config/types.py +5 -5
- provide/foundation/config/validators.py +4 -4
- provide/foundation/console/output.py +7 -7
- provide/foundation/context/core.py +19 -17
- provide/foundation/crypto/certificates/__init__.py +9 -5
- provide/foundation/crypto/certificates/base.py +2 -2
- provide/foundation/crypto/certificates/certificate.py +48 -19
- provide/foundation/crypto/certificates/factory.py +26 -18
- provide/foundation/crypto/certificates/generator.py +24 -23
- provide/foundation/crypto/certificates/loader.py +24 -16
- provide/foundation/crypto/certificates/operations.py +17 -10
- provide/foundation/crypto/certificates/trust.py +21 -21
- provide/foundation/env/__init__.py +28 -0
- provide/foundation/env/core.py +218 -0
- provide/foundation/errors/__init__.py +3 -2
- provide/foundation/errors/decorators.py +0 -3
- provide/foundation/errors/types.py +0 -1
- provide/foundation/eventsets/display.py +13 -14
- provide/foundation/eventsets/registry.py +61 -31
- provide/foundation/eventsets/resolver.py +50 -46
- provide/foundation/eventsets/sets/das.py +8 -8
- provide/foundation/eventsets/sets/database.py +14 -14
- provide/foundation/eventsets/sets/http.py +21 -21
- provide/foundation/eventsets/sets/llm.py +16 -16
- provide/foundation/eventsets/sets/task_queue.py +13 -13
- provide/foundation/eventsets/types.py +7 -7
- provide/foundation/file/directory.py +1 -1
- provide/foundation/file/lock.py +2 -3
- provide/foundation/hub/components.py +19 -21
- provide/foundation/hub/config.py +25 -19
- provide/foundation/hub/discovery.py +5 -4
- provide/foundation/hub/handlers.py +13 -5
- provide/foundation/hub/lifecycle.py +10 -9
- provide/foundation/hub/manager.py +3 -0
- provide/foundation/hub/processors.py +8 -3
- provide/foundation/integrations/__init__.py +1 -1
- provide/foundation/integrations/openobserve/client.py +2 -2
- provide/foundation/integrations/openobserve/commands.py +9 -9
- provide/foundation/integrations/openobserve/config.py +2 -2
- provide/foundation/integrations/openobserve/otlp.py +2 -2
- provide/foundation/integrations/openobserve/search.py +1 -2
- provide/foundation/integrations/openobserve/streaming.py +1 -1
- provide/foundation/logger/__init__.py +0 -1
- provide/foundation/logger/config/base.py +1 -1
- provide/foundation/logger/config/logging.py +19 -19
- provide/foundation/logger/config/telemetry.py +11 -13
- provide/foundation/logger/factories.py +2 -2
- provide/foundation/logger/processors/main.py +12 -10
- provide/foundation/logger/ratelimit/limiters.py +4 -4
- provide/foundation/logger/ratelimit/processor.py +1 -1
- provide/foundation/logger/setup/coordinator.py +38 -24
- provide/foundation/logger/setup/processors.py +3 -3
- provide/foundation/logger/setup/testing.py +14 -0
- provide/foundation/logger/trace.py +5 -5
- provide/foundation/metrics/__init__.py +1 -1
- provide/foundation/metrics/otel.py +3 -1
- provide/foundation/observability/__init__.py +1 -1
- provide/foundation/process/__init__.py +1 -1
- provide/foundation/process/exit.py +6 -5
- provide/foundation/process/lifecycle.py +41 -18
- provide/foundation/resilience/__init__.py +6 -5
- provide/foundation/resilience/circuit.py +32 -30
- provide/foundation/resilience/decorators.py +58 -42
- provide/foundation/resilience/fallback.py +55 -40
- provide/foundation/resilience/retry.py +67 -65
- provide/foundation/serialization/__init__.py +16 -0
- provide/foundation/serialization/core.py +70 -0
- provide/foundation/streams/config.py +8 -9
- provide/foundation/streams/console.py +3 -3
- provide/foundation/streams/core.py +2 -2
- provide/foundation/streams/file.py +1 -1
- provide/foundation/testing/__init__.py +22 -7
- provide/foundation/testing/archive/__init__.py +7 -7
- provide/foundation/testing/archive/fixtures.py +58 -54
- provide/foundation/testing/cli.py +3 -6
- provide/foundation/testing/common/__init__.py +13 -13
- provide/foundation/testing/common/fixtures.py +27 -30
- provide/foundation/testing/file/__init__.py +15 -15
- provide/foundation/testing/file/content_fixtures.py +65 -92
- provide/foundation/testing/file/directory_fixtures.py +19 -19
- provide/foundation/testing/file/fixtures.py +14 -17
- provide/foundation/testing/file/special_fixtures.py +34 -42
- provide/foundation/testing/logger.py +28 -23
- provide/foundation/testing/mocking/__init__.py +21 -21
- provide/foundation/testing/mocking/fixtures.py +80 -67
- provide/foundation/testing/process/__init__.py +23 -23
- provide/foundation/testing/process/async_fixtures.py +89 -80
- provide/foundation/testing/process/fixtures.py +11 -13
- provide/foundation/testing/process/subprocess_fixtures.py +41 -40
- provide/foundation/testing/threading/__init__.py +17 -17
- provide/foundation/testing/threading/basic_fixtures.py +21 -17
- provide/foundation/testing/threading/data_fixtures.py +18 -16
- provide/foundation/testing/threading/execution_fixtures.py +67 -52
- provide/foundation/testing/threading/fixtures.py +10 -14
- provide/foundation/testing/threading/sync_fixtures.py +21 -18
- provide/foundation/testing/time/__init__.py +11 -11
- provide/foundation/testing/time/fixtures.py +91 -79
- provide/foundation/testing/transport/__init__.py +9 -9
- provide/foundation/testing/transport/fixtures.py +54 -54
- provide/foundation/time/__init__.py +18 -0
- provide/foundation/time/core.py +63 -0
- provide/foundation/tools/__init__.py +2 -2
- provide/foundation/tools/base.py +68 -67
- provide/foundation/tools/cache.py +62 -69
- provide/foundation/tools/downloader.py +51 -56
- provide/foundation/tools/installer.py +51 -57
- provide/foundation/tools/registry.py +38 -45
- provide/foundation/tools/resolver.py +70 -68
- provide/foundation/tools/verifier.py +39 -50
- provide/foundation/tracer/spans.py +1 -13
- provide/foundation/transport/__init__.py +26 -33
- provide/foundation/transport/base.py +32 -30
- provide/foundation/transport/client.py +44 -49
- provide/foundation/transport/config.py +11 -13
- provide/foundation/transport/errors.py +13 -27
- provide/foundation/transport/http.py +69 -55
- provide/foundation/transport/middleware.py +86 -81
- provide/foundation/transport/registry.py +29 -27
- provide/foundation/transport/types.py +6 -6
- provide/foundation/utils/deps.py +3 -2
- provide/foundation/utils/parsing.py +7 -7
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/METADATA +2 -2
- provide_foundation-0.0.0.dev3.dist-info/RECORD +233 -0
- provide_foundation-0.0.0.dev2.dist-info/RECORD +0 -225
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/WHEEL +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/entry_points.txt +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/licenses/LICENSE +0 -0
- {provide_foundation-0.0.0.dev2.dist-info → provide_foundation-0.0.0.dev3.dist-info}/top_level.txt +0 -0
provide/foundation/tools/base.py
CHANGED
@@ -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
|
-
|
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
|
-
|
234
|
-
|
226
|
+
try:
|
227
|
+
if path.exists():
|
235
228
|
# Calculate directory size
|
236
229
|
if path.is_dir():
|
237
|
-
|
238
|
-
f.
|
239
|
-
|
240
|
-
|
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
|
-
|
245
|
-
|
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)
|