Coverage for openhcs/core/metadata_cache.py: 73.1%
56 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 18:33 +0000
1"""Simple metadata cache service for OpenHCS."""
3import threading
4from pathlib import Path
5from typing import Dict, Optional
7from openhcs.core.components.validation import convert_enum_by_value
8from openhcs.constants.constants import AllComponents
11class MetadataCache:
12 """Stores component key→name mappings with basic invalidation and thread safety."""
14 def __init__(self):
15 self._cache: Dict['AllComponents', Dict[str, Optional[str]]] = {}
16 self._metadata_file_mtimes: Dict[Path, float] = {}
17 self._lock = threading.Lock()
19 def cache_metadata(self, microscope_handler, plate_path: Path, component_keys_cache: Dict) -> None:
20 """Cache all metadata from metadata handler."""
21 with self._lock:
22 # Parse all metadata once
23 metadata = microscope_handler.metadata_handler.parse_metadata(plate_path)
25 # Initialize all components with keys mapped to None
26 for component in AllComponents:
27 component_keys = component_keys_cache.get(component, [])
28 self._cache[component] = {key: None for key in component_keys}
30 # Update with actual metadata where available
31 for component_name, mapping in metadata.items():
32 component = AllComponents(component_name)
33 if component in self._cache: 33 ↛ 41line 33 didn't jump to line 41 because the condition on line 33 was always true
34 combined_cache = self._cache[component].copy()
35 for metadata_key in mapping.keys():
36 if metadata_key not in combined_cache: 36 ↛ 37line 36 didn't jump to line 37 because the condition on line 36 was never true
37 combined_cache[metadata_key] = None
38 combined_cache.update(mapping)
39 self._cache[component] = combined_cache
40 else:
41 self._cache[component] = mapping
43 # Store metadata file mtime for invalidation
44 metadata_file = microscope_handler.metadata_handler.find_metadata_file(plate_path)
45 if metadata_file and metadata_file.exists(): 45 ↛ exitline 45 didn't jump to the function exit
46 self._metadata_file_mtimes[metadata_file] = metadata_file.stat().st_mtime
48 def get_component_metadata(self, component, key: str) -> Optional[str]:
49 """Get metadata display name for a component key. Accepts GroupBy or VariableComponents."""
50 with self._lock:
51 if not self._is_cache_valid():
52 self._cache.clear()
53 return None
55 # Convert GroupBy to AllComponents using OpenHCS generic utility
56 component = convert_enum_by_value(component, AllComponents) or component
58 return self._cache.get(component, {}).get(key)
60 def get_cached_metadata(self, component: 'AllComponents') -> Optional[Dict[str, Optional[str]]]:
61 """Get all cached metadata for a component."""
62 with self._lock:
63 if not self._is_cache_valid(): 63 ↛ 64line 63 didn't jump to line 64 because the condition on line 63 was never true
64 self._cache.clear()
65 return None
66 return self._cache.get(component)
68 def clear_cache(self) -> None:
69 """Clear cached metadata."""
70 with self._lock:
71 self._cache.clear()
72 self._metadata_file_mtimes.clear()
74 def _is_cache_valid(self) -> bool:
75 """Check if cache is valid by comparing file mtimes."""
76 for metadata_file, cached_mtime in self._metadata_file_mtimes.items():
77 if not metadata_file.exists() or metadata_file.stat().st_mtime != cached_mtime: 77 ↛ 78line 77 didn't jump to line 78 because the condition on line 77 was never true
78 return False
79 return True
82# Global cache instance
83_global_metadata_cache: Optional[MetadataCache] = None
86def get_metadata_cache() -> MetadataCache:
87 """Get global metadata cache instance."""
88 global _global_metadata_cache
89 if _global_metadata_cache is None:
90 _global_metadata_cache = MetadataCache()
91 return _global_metadata_cache