pig.py/neopig/async_filevault.py

221 lines
6 KiB
Python

"""
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)