diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0031779..0000000 --- a/.gitignore +++ /dev/null @@ -1,54 +0,0 @@ -# Byte-compiled / optimized -__pycache__/ -*.py[cod] -*$py.class - -# Distribution / packaging -dist/ -build/ -*.egg-info/ -*.egg - -# Virtual environments -.venv/ -venv/ -env/ - -# IDE -.idea/ -.vscode/ -*.swp -*.swo - -# Environment -.env - -# Database -*.db -*.sqlite3 - -# Local / generated artifacts -.codex -.claude/ -docs/internal/ -docs/OPTIMIZATION_ROADMAP.md -docs/demo.cast -reports/ -!docs/reports/ -docs/reports/* -!docs/reports/showcase_*.md -results/ - -# Research papers (private, not for public distribution) -papers/ - -# OS -.DS_Store -Thumbs.db - -# Test cache -.pytest_cache/ -.mypy_cache/ -.ruff_cache/ -htmlcov/ -.coverage diff --git a/pyproject.toml b/pyproject.toml index ac3b9c5..fed90f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "pandas>=2.0", "pyyaml>=6.0", "tenacity>=8.2", + "filelock>=3.12", ] [project.optional-dependencies] diff --git a/src/alphaevo/data/cache.py b/src/alphaevo/data/cache.py index b2c3957..230da71 100644 --- a/src/alphaevo/data/cache.py +++ b/src/alphaevo/data/cache.py @@ -11,7 +11,6 @@ from __future__ import annotations import contextlib -import fcntl import logging import time from collections.abc import Iterator @@ -20,6 +19,7 @@ from pathlib import Path import pandas as pd # type: ignore[import-untyped] +from filelock import FileLock logger = logging.getLogger(__name__) @@ -162,19 +162,15 @@ def put(self, symbol: str, start: date, end: date, df: pd.DataFrame) -> None: @staticmethod @contextmanager def _file_lock(target: Path) -> Iterator[None]: - """Acquire an exclusive file lock adjacent to *target*.""" + """Acquire an exclusive file lock adjacent to *target* (cross-platform).""" lock_path = target.with_suffix(target.suffix + ".lock") lock_path.parent.mkdir(parents=True, exist_ok=True) - fd = lock_path.open("w") - try: - fcntl.flock(fd, fcntl.LOCK_EX) + lock = FileLock(str(lock_path)) + with lock: yield - finally: - fcntl.flock(fd, fcntl.LOCK_UN) - fd.close() - # Best-effort cleanup; race with other processes is harmless - with contextlib.suppress(OSError): - lock_path.unlink() + # Best-effort cleanup; race with other processes is harmless + with contextlib.suppress(OSError): + lock_path.unlink() def invalidate(self, symbol: str | None = None) -> int: """Clear cached data.