- Fix TOCTOU race conditions by wrapping read-modify-write cycles under single-file locks in execution_state, portfolio_service, precheck_state, state_manager, and precheck_service. - Add missing test coverage (96 tests total): - test_review_service.py (15 tests) - test_check_api.py (6 tests) - test_external_gate.py main branches (+10 tests) - test_trade_execution.py new commands (+8 tests) - Unify all agent-consumed JSON messages to English. - Config-ize hardcoded values (volume filter, schema_version) via get_user_config with sensible defaults. - Add 1-hour TTL to exchange cache with force_new override. - Add ruff and mypy to dev dependencies; fix all type errors. - Add __all__ declarations to 11 service modules. - Sync README with new commands, config tuning docs, and PyPI badge. - Publish package as coinhunter==1.0.0 on PyPI with MIT license. - Deprecate coinhunter-cli==1.0.1 with runtime warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
77 lines
2.1 KiB
Python
77 lines
2.1 KiB
Python
"""File locking and atomic JSON helpers."""
|
|
import fcntl
|
|
import json
|
|
import os
|
|
|
|
__all__ = [
|
|
"locked_file",
|
|
"atomic_write_json",
|
|
"load_json_locked",
|
|
"save_json_locked",
|
|
"read_modify_write_json",
|
|
]
|
|
from contextlib import contextmanager
|
|
from pathlib import Path
|
|
|
|
|
|
@contextmanager
|
|
def locked_file(path: Path):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
fd = None
|
|
try:
|
|
fd = os.open(path, os.O_RDWR | os.O_CREAT)
|
|
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
yield fd
|
|
finally:
|
|
if fd is not None:
|
|
try:
|
|
os.fsync(fd)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
fcntl.flock(fd, fcntl.LOCK_UN)
|
|
except Exception:
|
|
pass
|
|
os.close(fd)
|
|
|
|
|
|
def atomic_write_json(path: Path, data: dict):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def load_json_locked(path: Path, lock_path: Path, default):
|
|
with locked_file(lock_path):
|
|
if not path.exists():
|
|
return default
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
return default
|
|
|
|
|
|
def save_json_locked(path: Path, lock_path: Path, data: dict):
|
|
with locked_file(lock_path):
|
|
atomic_write_json(path, data)
|
|
|
|
|
|
def read_modify_write_json(path: Path, lock_path: Path, default, modifier):
|
|
"""Atomic read-modify-write under a single file lock.
|
|
|
|
Loads JSON from *path* (or uses *default* if missing/invalid),
|
|
calls ``modifier(data)``, then atomically writes the result back.
|
|
If *modifier* returns None, the mutated *data* is written.
|
|
"""
|
|
with locked_file(lock_path):
|
|
if path.exists():
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except Exception:
|
|
data = default
|
|
else:
|
|
data = default
|
|
result = modifier(data)
|
|
atomic_write_json(path, result if result is not None else data)
|