""" AsyncFileVault 1.1.0 - Async wrapper for FileVault Provides async versions of FileVault I/O operations using asyncio.to_thread(). CPU-bound operations (hash generation, path creation) remain synchronous. Usage: vault = AsyncVault(vaultpath="vault", depth=9, use_pairs=True) # Sync operations (fast, no I/O) filename = vault.create_filename("seed", ".json", absolute=True) # Async operations (file I/O) await vault.atomic_write_json(filename, {"key": "value"}) data = await vault.safe_read_json(filename) """ import asyncio from functools import partial from typing import Any, Optional, List from filevault import Vault, ensure_bytes, create_vault __version__ = "1.1.0" __author__ = "Russell Ballestrini" __license__ = "Public Domain" class AsyncVault(Vault): """ Async wrapper for Vault with non-blocking file I/O. Inherits all sync methods from Vault but provides async versions of file operations that use asyncio.to_thread() to avoid blocking. Sync methods (instant, no await needed): - create_filename() - create_random_filename() - _generate_filename() - mark_file_exists() - clear_existence_cache() - get_cache_stats() - purge_cache_for_path_pattern() Async methods (await required): - atomic_write_json() - safe_read_json() - write_text_file() - read_text_file() - file_exists() - remove_file() - remove_file_if_exists() """ def __init__( self, vaultpath: str = "vault", depth: int = 3, salt: str = "changeme", enable_memory_cache: bool = False, use_pairs: bool = False ): """ Initialize AsyncVault with same parameters as Vault. Args: vaultpath: Base path for vault storage depth: Directory tree depth salt: Salt for hash generation enable_memory_cache: Enable in-memory file existence cache use_pairs: Use hex pairs for directories (256 dirs/level vs 16) """ super().__init__( vaultpath=vaultpath, depth=depth, salt=salt, enable_memory_cache=enable_memory_cache, use_pairs=use_pairs ) async def atomic_write_json( self, file_path: str, data: Any, indent: int = 2, aliases: Optional[List[str]] = None ) -> None: """ Async version of atomic JSON write. Args: file_path: Target file path data: Data to write as JSON indent: JSON indentation level aliases: Optional list of alias keys for symlinks """ await asyncio.to_thread( super().atomic_write_json, file_path, data, indent, aliases ) async def safe_read_json( self, file_path: str, default: Any = None ) -> Any: """ Async version of safe JSON read. Args: file_path: Path to JSON file default: Default value if file doesn't exist or is invalid Returns: Parsed JSON data or default value """ return await asyncio.to_thread( super().safe_read_json, file_path, default ) async def write_text_file( self, file_path: str, content: str, mode: str = "w" ) -> None: """ Async version of text file write. Args: file_path: Target file path content: Text content to write mode: File open mode ('w', 'a', etc.) """ await asyncio.to_thread( super().write_text_file, file_path, content, mode ) async def read_text_file( self, file_path: str, default: Optional[str] = None ) -> str: """ Async version of text file read. Args: file_path: Path to text file default: Default value if file doesn't exist Returns: File content or default value """ return await asyncio.to_thread( super().read_text_file, file_path, default ) async def file_exists(self, file_path: str) -> bool: """ Async version of file existence check. Note: If memory cache is enabled and the path is cached, this returns immediately from cache without blocking. Args: file_path: Path to check Returns: True if file exists, False otherwise """ # Check cache first (no I/O needed) if self.enable_memory_cache and self._existence_cache is not None: if file_path in self._existence_cache: return self._existence_cache[file_path] # Cache miss - need to hit filesystem return await asyncio.to_thread(super().file_exists, file_path) async def remove_file(self, file_path: str) -> bool: """ Async version of file removal. Args: file_path: Path to file to remove Returns: True if file was removed, False if it didn't exist """ # Call parent's sync remove_file directly via Vault class return await asyncio.to_thread(Vault.remove_file, self, file_path) async def remove_file_if_exists(self, file_path: str) -> bool: """ Async version of remove_file_if_exists. Args: file_path: Path to file to remove Returns: True if file was removed, False if it didn't exist """ try: return await asyncio.to_thread(Vault.remove_file, self, file_path) except OSError: self.mark_file_exists(file_path, False) return False def create_async_vault(*args, **kwargs) -> AsyncVault: """Factory function for creating AsyncVault instances.""" return AsyncVault(*args, **kwargs)