Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/telemetry_window_demo/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def load_feature_table(path: str | Path) -> pd.DataFrame:
FEATURE_TABLE_NUMERIC_COLUMNS,
source=str(table_path),
)
_require_window_bounds(frame, source=str(table_path))
return frame


Expand All @@ -120,6 +121,8 @@ def load_alert_table(path: str | Path) -> pd.DataFrame:
ALERT_TABLE_DATETIME_COLUMNS,
source=str(table_path),
)
_require_window_bounds(frame, source=str(table_path))
_require_alert_time_bounds(frame, source=str(table_path))
_require_text_columns(frame, ALERT_TABLE_TEXT_COLUMNS, source=str(table_path))
return frame

Expand Down Expand Up @@ -209,6 +212,26 @@ def _parse_numeric_columns(
frame[column] = parsed.astype("int64" if require_integer else "float64")


def _require_window_bounds(frame: pd.DataFrame, *, source: str) -> None:
invalid_windows = frame["window_end"] <= frame["window_start"]
if invalid_windows.any():
raise ValueError(
f"Window end must be after window start in {source}: "
f"{int(invalid_windows.sum())} row(s)"
)


def _require_alert_time_bounds(frame: pd.DataFrame, *, source: str) -> None:
out_of_bounds = (frame["alert_time"] < frame["window_start"]) | (
frame["alert_time"] > frame["window_end"]
)
if out_of_bounds.any():
raise ValueError(
f"Alert time must fall within window bounds in {source}: "
f"{int(out_of_bounds.sum())} row(s)"
)


def _require_text_columns(
frame: pd.DataFrame,
text_columns: tuple[str, ...],
Expand Down
50 changes: 50 additions & 0 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,22 @@ def test_load_feature_table_rejects_out_of_range_error_rate(tmp_path) -> None:
assert "error_rate" in message


def test_load_feature_table_rejects_non_positive_window_bounds(tmp_path) -> None:
path = tmp_path / "features.csv"
path.write_text(
"window_start,window_end,event_count,error_rate\n"
"2026-03-10T10:01:00Z,2026-03-10T10:01:00Z,10,0.25\n",
encoding="utf-8",
)

with pytest.raises(ValueError) as excinfo:
load_feature_table(path)

message = str(excinfo.value)
assert "Window end must be after window start" in message
assert "1 row(s)" in message


def test_load_alert_table_rejects_invalid_csv(tmp_path) -> None:
path = tmp_path / "alerts.csv"
path.write_text(
Expand Down Expand Up @@ -344,6 +360,40 @@ def test_load_alert_table_rejects_missing_alert_timestamp(tmp_path) -> None:
assert "alert_time" in message


def test_load_alert_table_rejects_non_positive_window_bounds(tmp_path) -> None:
path = tmp_path / "alerts.csv"
path.write_text(
"alert_time,window_start,window_end,rule_name,severity\n"
"2026-03-10T10:00:30Z,2026-03-10T10:01:00Z,"
"2026-03-10T10:00:00Z,high_error_rate,medium\n",
encoding="utf-8",
)

with pytest.raises(ValueError) as excinfo:
load_alert_table(path)

message = str(excinfo.value)
assert "Window end must be after window start" in message
assert "1 row(s)" in message


def test_load_alert_table_rejects_alert_time_outside_window(tmp_path) -> None:
path = tmp_path / "alerts.csv"
path.write_text(
"alert_time,window_start,window_end,rule_name,severity\n"
"2026-03-10T10:02:00Z,2026-03-10T10:00:00Z,"
"2026-03-10T10:01:00Z,high_error_rate,medium\n",
encoding="utf-8",
)

with pytest.raises(ValueError) as excinfo:
load_alert_table(path)

message = str(excinfo.value)
assert "Alert time must fall within window bounds" in message
assert "1 row(s)" in message


def test_load_alert_table_rejects_missing_rule_name(tmp_path) -> None:
path = tmp_path / "alerts.csv"
path.write_text(
Expand Down
Loading