350 lines
12 KiB
Python
350 lines
12 KiB
Python
"""Generic caching functionality for OntoCast tools.
|
|
|
|
This module provides a generic caching mechanism that can be used by various
|
|
tools to cache their results based on input content and configuration parameters.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ontocast.util import render_text_hash
|
|
|
|
if TYPE_CHECKING:
|
|
from ontocast.config import Config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_default_cache_dir() -> Path:
|
|
"""Get the default cache directory based on the environment.
|
|
|
|
Returns:
|
|
Path: The appropriate cache directory path.
|
|
"""
|
|
# Check if we're in a test environment
|
|
if "pytest" in os.environ.get("_", ""):
|
|
# In tests, use a test-specific cache directory
|
|
return Path.cwd() / ".test_cache"
|
|
|
|
# Check for common cache environment variables
|
|
cache_home = os.environ.get("XDG_CACHE_HOME")
|
|
if cache_home:
|
|
return Path(cache_home) / "ontocast"
|
|
|
|
# Use platform-appropriate cache directory
|
|
if os.name == "nt": # Windows
|
|
cache_dir = Path.home() / "AppData" / "Local" / "ontocast"
|
|
else: # Unix-like systems
|
|
cache_dir = Path.home() / ".cache" / "ontocast"
|
|
|
|
return cache_dir
|
|
|
|
|
|
class Cacher:
|
|
"""Shared caching class for OntoCast tools.
|
|
|
|
This class provides a unified interface for caching results from various
|
|
tools based on input content and configuration parameters. It manages
|
|
multiple subdirectories for different tools from a single instance.
|
|
|
|
Attributes:
|
|
cache_dir: Base directory for caching.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
cache_dir: str | Path | None = None,
|
|
config: "Config | None" = None,
|
|
):
|
|
"""Initialize the shared cacher.
|
|
|
|
Args:
|
|
cache_dir: Base directory for caching. If None, uses config or platform-appropriate default.
|
|
config: Optional config object to get cache_dir from.
|
|
"""
|
|
if cache_dir is None and config is not None:
|
|
# Try to get cache_dir from config
|
|
if hasattr(config, "tool_config") and hasattr(
|
|
config.tool_config, "path_config"
|
|
):
|
|
cache_dir = config.tool_config.path_config.cache_dir
|
|
|
|
if cache_dir is None:
|
|
cache_dir = _get_default_cache_dir()
|
|
|
|
self.cache_dir = Path(cache_dir).expanduser()
|
|
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
logger.debug(f"Shared cache directory set to: {self.cache_dir}")
|
|
|
|
def _get_tool_cache_dir(self, subdirectory: str) -> Path:
|
|
"""Get the cache directory for a specific tool subdirectory.
|
|
|
|
Args:
|
|
subdirectory: The tool subdirectory name.
|
|
|
|
Returns:
|
|
Path: The full path to the tool's cache directory.
|
|
"""
|
|
tool_cache_dir = self.cache_dir / subdirectory
|
|
tool_cache_dir.mkdir(parents=True, exist_ok=True)
|
|
return tool_cache_dir
|
|
|
|
def _generate_cache_key(
|
|
self,
|
|
content: str | bytes,
|
|
config: dict[str, str | int | float | bool] | None = None,
|
|
**kwargs: str | int | float | bool,
|
|
) -> str:
|
|
"""Generate a cache key based on content and configuration.
|
|
|
|
Args:
|
|
content: The input content (text, bytes, etc.).
|
|
config: Optional configuration dictionary.
|
|
**kwargs: Additional parameters that affect the result.
|
|
|
|
Returns:
|
|
str: A hash string to use as cache key.
|
|
"""
|
|
# Convert content to string for hashing
|
|
if isinstance(content, bytes):
|
|
content_str = content.decode("utf-8", errors="ignore")
|
|
else:
|
|
content_str = str(content)
|
|
|
|
# Create a dictionary with all relevant parameters
|
|
cache_data = {
|
|
"content": content_str,
|
|
"config": config or {},
|
|
"kwargs": kwargs,
|
|
}
|
|
|
|
# Convert to JSON string and hash it
|
|
cache_string = json.dumps(cache_data, sort_keys=True, default=str)
|
|
return render_text_hash(cache_string, digits=None)
|
|
|
|
def _get_cache_file_path(self, cache_key: str, subdirectory: str) -> Path:
|
|
"""Get the cache file path for a given cache key and subdirectory.
|
|
|
|
Args:
|
|
cache_key: The cache key.
|
|
subdirectory: The tool subdirectory name.
|
|
|
|
Returns:
|
|
Path: The path to the cache file.
|
|
"""
|
|
tool_cache_dir = self._get_tool_cache_dir(subdirectory)
|
|
return tool_cache_dir / f"{cache_key}.json"
|
|
|
|
def get(
|
|
self,
|
|
content: str | bytes,
|
|
subdirectory: str,
|
|
config: dict[str, str | int | float | bool] | None = None,
|
|
**kwargs: str | int | float | bool,
|
|
) -> str | dict | list | None:
|
|
"""Get cached result for given content and configuration.
|
|
|
|
Args:
|
|
content: The input content.
|
|
subdirectory: The tool subdirectory name.
|
|
config: Optional configuration dictionary.
|
|
**kwargs: Additional parameters that affect the result.
|
|
|
|
Returns:
|
|
Optional[Any]: The cached result or None if not found.
|
|
"""
|
|
cache_key = self._generate_cache_key(content, config, **kwargs)
|
|
cache_file = self._get_cache_file_path(cache_key, subdirectory)
|
|
|
|
if not cache_file.exists():
|
|
return None
|
|
|
|
try:
|
|
with open(cache_file, "r", encoding="utf-8") as f:
|
|
cached_data = json.load(f)
|
|
logger.debug(f"Cache hit for key: {cache_key[:16]}...")
|
|
return cached_data.get("result")
|
|
except (json.JSONDecodeError, IOError) as e:
|
|
logger.warning(f"Failed to read cache file {cache_file}: {e}")
|
|
return None
|
|
|
|
def set(
|
|
self,
|
|
content: str | bytes,
|
|
result: str | dict | list,
|
|
subdirectory: str,
|
|
config: dict[str, str | int | float | bool] | None = None,
|
|
**kwargs: str | int | float | bool,
|
|
) -> None:
|
|
"""Cache a result for given content and configuration.
|
|
|
|
Args:
|
|
content: The input content.
|
|
result: The result to cache.
|
|
subdirectory: The tool subdirectory name.
|
|
config: Optional configuration dictionary.
|
|
**kwargs: Additional parameters that affect the result.
|
|
"""
|
|
cache_key = self._generate_cache_key(content, config, **kwargs)
|
|
cache_file = self._get_cache_file_path(cache_key, subdirectory)
|
|
|
|
# Prepare data for caching
|
|
cache_data = {
|
|
"result": result,
|
|
"content": str(content)[:100] + "..."
|
|
if len(str(content)) > 100
|
|
else str(content),
|
|
"config": config or {},
|
|
"kwargs": kwargs,
|
|
}
|
|
|
|
try:
|
|
with open(cache_file, "w", encoding="utf-8") as f:
|
|
json.dump(cache_data, f, indent=2, default=str)
|
|
logger.debug(f"Cached result to {cache_file}")
|
|
except IOError as e:
|
|
logger.warning(f"Failed to write cache file {cache_file}: {e}")
|
|
|
|
def clear(self, subdirectory: str | None = None) -> None:
|
|
"""Clear cached results.
|
|
|
|
Args:
|
|
subdirectory: If provided, clear only this subdirectory. If None, clear all.
|
|
"""
|
|
if subdirectory is None:
|
|
# Clear all subdirectories
|
|
if self.cache_dir.exists():
|
|
for cache_file in self.cache_dir.glob("**/*.json"):
|
|
cache_file.unlink()
|
|
logger.info(f"Cleared all cache directories: {self.cache_dir}")
|
|
else:
|
|
# Clear specific subdirectory
|
|
tool_cache_dir = self._get_tool_cache_dir(subdirectory)
|
|
if tool_cache_dir.exists():
|
|
for cache_file in tool_cache_dir.glob("*.json"):
|
|
cache_file.unlink()
|
|
logger.info(f"Cleared cache directory: {tool_cache_dir}")
|
|
|
|
def get_cache_stats(
|
|
self, subdirectory: str | None = None
|
|
) -> dict[str, int | dict[str, int]]:
|
|
"""Get cache statistics.
|
|
|
|
Args:
|
|
subdirectory: If provided, get stats for this subdirectory only. If None, get stats for all.
|
|
|
|
Returns:
|
|
Dict[str, Any]: Dictionary with cache statistics.
|
|
"""
|
|
if subdirectory is None:
|
|
# Get stats for all subdirectories
|
|
if not self.cache_dir.exists():
|
|
return {"total_files": 0, "total_size_bytes": 0, "subdirectories": {}}
|
|
|
|
cache_files = list(self.cache_dir.glob("**/*.json"))
|
|
total_size = sum(f.stat().st_size for f in cache_files)
|
|
|
|
# Group by subdirectory
|
|
subdir_stats = {}
|
|
for cache_file in cache_files:
|
|
subdir = cache_file.parent.name
|
|
if subdir not in subdir_stats:
|
|
subdir_stats[subdir] = {"files": 0, "size_bytes": 0}
|
|
subdir_stats[subdir]["files"] += 1
|
|
subdir_stats[subdir]["size_bytes"] += cache_file.stat().st_size
|
|
|
|
return {
|
|
"total_files": len(cache_files),
|
|
"total_size_bytes": total_size,
|
|
"subdirectories": subdir_stats,
|
|
}
|
|
else:
|
|
# Get stats for specific subdirectory
|
|
tool_cache_dir = self._get_tool_cache_dir(subdirectory)
|
|
if not tool_cache_dir.exists():
|
|
return {"total_files": 0, "total_size_bytes": 0}
|
|
|
|
cache_files = list(tool_cache_dir.glob("*.json"))
|
|
total_size = sum(f.stat().st_size for f in cache_files)
|
|
|
|
return {
|
|
"total_files": len(cache_files),
|
|
"total_size_bytes": total_size,
|
|
}
|
|
|
|
|
|
class ToolCacher:
|
|
"""Tool-specific wrapper for the shared Cacher.
|
|
|
|
This class provides a tool-specific interface to the shared Cacher,
|
|
automatically handling the subdirectory parameter.
|
|
"""
|
|
|
|
def __init__(self, shared_cacher: Cacher, subdirectory: str):
|
|
"""Initialize the tool cacher.
|
|
|
|
Args:
|
|
shared_cacher: The shared Cacher instance.
|
|
subdirectory: The subdirectory name for this tool.
|
|
"""
|
|
self.shared_cacher = shared_cacher
|
|
self.subdirectory = subdirectory
|
|
|
|
def get(
|
|
self,
|
|
content: str | bytes,
|
|
config: dict[str, str | int | float | bool] | None = None,
|
|
**kwargs: str | int | float | bool,
|
|
) -> str | dict | list | None:
|
|
"""Get cached result for given content and configuration.
|
|
|
|
Args:
|
|
content: The input content.
|
|
config: Optional configuration dictionary.
|
|
**kwargs: Additional parameters that affect the result.
|
|
|
|
Returns:
|
|
Optional[Any]: The cached result or None if not found.
|
|
"""
|
|
return self.shared_cacher.get(
|
|
content=content, subdirectory=self.subdirectory, config=config, **kwargs
|
|
)
|
|
|
|
def set(
|
|
self,
|
|
content: str | bytes,
|
|
result: str | dict | list,
|
|
config: dict[str, str | int | float | bool] | None = None,
|
|
**kwargs: str | int | float | bool,
|
|
) -> None:
|
|
"""Cache a result for given content and configuration.
|
|
|
|
Args:
|
|
content: The input content.
|
|
result: The result to cache.
|
|
config: Optional configuration dictionary.
|
|
**kwargs: Additional parameters that affect the result.
|
|
"""
|
|
self.shared_cacher.set(
|
|
content=content,
|
|
result=result,
|
|
subdirectory=self.subdirectory,
|
|
config=config,
|
|
**kwargs,
|
|
)
|
|
|
|
def clear(self) -> None:
|
|
"""Clear cached results for this tool."""
|
|
self.shared_cacher.clear(subdirectory=self.subdirectory)
|
|
|
|
def get_cache_stats(self) -> dict[str, int | dict[str, int]]:
|
|
"""Get cache statistics for this tool.
|
|
|
|
Returns:
|
|
Dict[str, int]: Dictionary with cache statistics.
|
|
"""
|
|
return self.shared_cacher.get_cache_stats(subdirectory=self.subdirectory)
|