164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
"""Contract tests for data/exceptions.py.
|
|
|
|
Every loader in data/ raises DataNotFoundError with a specific combination
|
|
of keyword arguments (download_script vs instructions vs download_url, plus
|
|
readme). The tests below pin the message shape so a reformat of _build_message
|
|
does not silently drop the reader-facing download instructions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from data.exceptions import DataNotFoundError, DownloadError, MissingDependencyError
|
|
|
|
|
|
def test_data_not_found_with_download_script_shows_command() -> None:
|
|
err = DataNotFoundError(
|
|
dataset_name="ETF Universe",
|
|
path=Path("/data/etfs/market/etf_universe.parquet"),
|
|
download_script="data/etfs/market/download.py",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "DATA NOT FOUND: ETF Universe" in msg
|
|
assert "/data/etfs/market/etf_universe.parquet" in msg
|
|
assert "uv run python data/etfs/market/download.py" in msg
|
|
assert "data/README.md" in msg # Default readme fallback
|
|
|
|
|
|
def test_data_not_found_with_custom_readme_overrides_default() -> None:
|
|
err = DataNotFoundError(
|
|
dataset_name="SEC Filings",
|
|
path=Path("/data/equities/fundamentals/"),
|
|
download_script="data/equities/fundamentals/filings_download.py",
|
|
readme="data/equities/fundamentals/README.md",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "data/equities/fundamentals/README.md" in msg
|
|
# Default readme should not appear when custom is set
|
|
assert msg.count("README.md") == 1
|
|
|
|
|
|
def test_data_not_found_with_download_url_prefers_url_over_script() -> None:
|
|
err = DataNotFoundError(
|
|
dataset_name="AlgoSeek Options",
|
|
path=Path("/data/options/sp500_options.parquet"),
|
|
download_url="https://example.com/algoseek.zip",
|
|
download_script="data/ignored/download.py", # Should be ignored
|
|
requires_api_key="ALGOSEEK_KEY",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "https://example.com/algoseek.zip" in msg
|
|
# download_script is shadowed by download_url in the elif chain
|
|
assert "uv run python data/ignored/download.py" not in msg
|
|
assert "Extract to:" in msg
|
|
assert "ALGOSEEK_KEY" in msg
|
|
|
|
|
|
def test_data_not_found_with_instructions_overrides_other_branches() -> None:
|
|
err = DataNotFoundError(
|
|
dataset_name="Derived Daily Bars",
|
|
path=Path("/data/futures/market/continuous/daily/continuous_daily.parquet"),
|
|
instructions="Run: python 02/05_futures_session_aggregation.py",
|
|
download_script="should_not_appear.py",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "02/05_futures_session_aggregation.py" in msg
|
|
assert "should_not_appear.py" not in msg
|
|
|
|
|
|
def test_data_not_found_with_derivation_notebook_adds_pointer() -> None:
|
|
err = DataNotFoundError(
|
|
dataset_name="Derived Label",
|
|
path=Path("/data/labels/fwd_ret_5d.parquet"),
|
|
derivation_notebook="07_defining_the_learning_task/03_label_methods.py",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "How this dataset is built" in msg
|
|
assert "07_defining_the_learning_task/03_label_methods.py" in msg
|
|
|
|
|
|
def test_data_not_found_is_filenotfounderror() -> None:
|
|
"""Callers catch FileNotFoundError generically; guard the inheritance."""
|
|
err = DataNotFoundError(
|
|
dataset_name="x",
|
|
path=Path("/tmp/x"),
|
|
download_script="x.py",
|
|
)
|
|
|
|
assert isinstance(err, FileNotFoundError)
|
|
with pytest.raises(FileNotFoundError):
|
|
raise err
|
|
|
|
|
|
def test_data_not_found_accepts_str_path() -> None:
|
|
"""Some callers pass str paths; the class must normalize to Path."""
|
|
err = DataNotFoundError(
|
|
dataset_name="x",
|
|
path="/tmp/string_path.parquet",
|
|
download_script="x.py",
|
|
)
|
|
|
|
assert isinstance(err.path, Path)
|
|
assert "/tmp/string_path.parquet" in str(err)
|
|
|
|
|
|
def test_download_error_includes_reason_and_suggestion() -> None:
|
|
err = DownloadError(
|
|
dataset_name="Crypto Perps",
|
|
reason="API returned 429",
|
|
suggestion="Retry after backoff or check rate limits",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "DOWNLOAD FAILED: Crypto Perps" in msg
|
|
assert "API returned 429" in msg
|
|
assert "Retry after backoff" in msg
|
|
|
|
|
|
def test_download_error_without_suggestion_still_valid() -> None:
|
|
err = DownloadError(dataset_name="x", reason="unknown")
|
|
msg = str(err)
|
|
|
|
assert "DOWNLOAD FAILED: x" in msg
|
|
assert "Suggestion:" not in msg
|
|
|
|
|
|
def test_download_error_is_runtime_error() -> None:
|
|
err = DownloadError(dataset_name="x", reason="y")
|
|
assert isinstance(err, RuntimeError)
|
|
|
|
|
|
def test_missing_dependency_error_defaults_install_command() -> None:
|
|
err = MissingDependencyError(package="edgartools")
|
|
msg = str(err)
|
|
|
|
assert "edgartools" in msg
|
|
assert "pip install edgartools" in msg
|
|
|
|
|
|
def test_missing_dependency_error_custom_install_and_purpose() -> None:
|
|
err = MissingDependencyError(
|
|
package="torch",
|
|
install_command="uv sync --extra gpu",
|
|
purpose="LSTM training in Ch13",
|
|
)
|
|
msg = str(err)
|
|
|
|
assert "torch" in msg
|
|
assert "uv sync --extra gpu" in msg
|
|
assert "LSTM training in Ch13" in msg
|
|
|
|
|
|
def test_missing_dependency_is_import_error() -> None:
|
|
"""Callers catch ImportError to gate optional backends."""
|
|
err = MissingDependencyError(package="x")
|
|
assert isinstance(err, ImportError)
|