Initial public release
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import aiofiles.os
|
||||
import msgspec
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_file_locks: dict[str, threading.Lock] = {}
|
||||
_locks_lock = threading.Lock()
|
||||
_async_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def _get_file_lock(file_path: Path) -> threading.Lock:
|
||||
path_str = str(file_path.resolve())
|
||||
with _locks_lock:
|
||||
if path_str not in _file_locks:
|
||||
_file_locks[path_str] = threading.Lock()
|
||||
return _file_locks[path_str]
|
||||
|
||||
|
||||
def _get_async_lock(file_path: Path) -> asyncio.Lock:
|
||||
path_str = str(file_path.resolve())
|
||||
if path_str not in _async_locks:
|
||||
_async_locks[path_str] = asyncio.Lock()
|
||||
return _async_locks[path_str]
|
||||
|
||||
|
||||
def atomic_write_json(file_path: Path, data: Any, indent: int = 2) -> None:
|
||||
lock = _get_file_lock(file_path)
|
||||
|
||||
with lock:
|
||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp_path = file_path.with_suffix(file_path.suffix + '.tmp')
|
||||
|
||||
try:
|
||||
_ = indent
|
||||
with open(tmp_path, 'wb') as f:
|
||||
f.write(msgspec.json.encode(data))
|
||||
f.flush()
|
||||
|
||||
tmp_path.replace(file_path)
|
||||
logger.debug(f"Atomically wrote JSON to {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
if tmp_path.exists():
|
||||
try:
|
||||
tmp_path.unlink()
|
||||
except OSError as cleanup_error:
|
||||
logger.warning("Couldn't remove temp file %s: %s", tmp_path, cleanup_error)
|
||||
logger.error(f"Failed to write JSON to {file_path}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def atomic_write_json_async(file_path: Path, data: Any, indent: int = 2) -> None:
|
||||
lock = _get_async_lock(file_path)
|
||||
|
||||
async with lock:
|
||||
await aiofiles.os.makedirs(file_path.parent, exist_ok=True)
|
||||
tmp_path = file_path.with_suffix(file_path.suffix + '.tmp')
|
||||
|
||||
try:
|
||||
_ = indent
|
||||
content = msgspec.json.encode(data)
|
||||
async with aiofiles.open(tmp_path, 'wb') as f:
|
||||
await f.write(content)
|
||||
|
||||
await asyncio.to_thread(tmp_path.replace, file_path)
|
||||
logger.debug(f"Atomically wrote JSON to {file_path}")
|
||||
|
||||
except Exception as e:
|
||||
try:
|
||||
if tmp_path.exists():
|
||||
await aiofiles.os.remove(tmp_path)
|
||||
except OSError as cleanup_error:
|
||||
logger.warning("Couldn't remove temp file %s: %s", tmp_path, cleanup_error)
|
||||
logger.error(f"Failed to write JSON to {file_path}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def read_json(file_path: Path, default: Any = None) -> Any:
|
||||
lock = _get_file_lock(file_path)
|
||||
|
||||
with lock:
|
||||
if not file_path.exists():
|
||||
return default
|
||||
|
||||
with open(file_path, 'rb') as f:
|
||||
return msgspec.json.decode(f.read())
|
||||
|
||||
|
||||
async def read_json_async(file_path: Path, default: Any = None) -> Any:
|
||||
lock = _get_async_lock(file_path)
|
||||
|
||||
async with lock:
|
||||
if not file_path.exists():
|
||||
return default
|
||||
|
||||
async with aiofiles.open(file_path, 'rb') as f:
|
||||
content = await f.read()
|
||||
return msgspec.json.decode(content)
|
||||
Reference in New Issue
Block a user