refactor: address high-priority debt and publish to PyPI
- 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>
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
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
|
||||
|
||||
@@ -9,13 +17,22 @@ from pathlib import Path
|
||||
@contextmanager
|
||||
def locked_file(path: Path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(path, "a+", encoding="utf-8") as f:
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
||||
f.seek(0)
|
||||
yield f
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
||||
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):
|
||||
@@ -38,3 +55,22 @@ def load_json_locked(path: Path, lock_path: Path, 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)
|
||||
|
||||
Reference in New Issue
Block a user