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
9 changes: 5 additions & 4 deletions usaspending_api/llm/assistants/filter_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ def _fiscal_year_date_context() -> str:
current_fy = today.year + 1 if today.month >= 10 else today.year
return (
f"\nThe current date is {today.strftime('%m/%d/%Y')}. The current federal fiscal year is "
f"FY{current_fy} (the federal fiscal year runs Oct 1 - Sep 30 and is named for the calendar "
f"year it ends in, so FY{current_fy} runs 10/01/{current_fy - 1} - 09/30/{current_fy}). "
f"Use FY{current_fy} directly as 'this fiscal year' and FY{current_fy - 1} as 'last fiscal "
f"year' - do not recompute the fiscal year from the date yourself."
f"{current_fy} (the federal fiscal year runs Oct 1 - Sep 30 and is named for the calendar "
f"year it ends in, so Fiscal Year {current_fy} runs 10/01/{current_fy - 1} - 09/30/{current_fy}). "
f"Use {current_fy} directly as 'this fiscal year' and {current_fy - 1} as 'last fiscal "
f"year' - do not recompute the fiscal year from the date yourself. When populating "
f"timePeriodFY, use the bare four-digit year (e.g. '{current_fy}'), never the 'FY' prefix."
)

@cached_property
Expand Down
13 changes: 13 additions & 0 deletions usaspending_api/llm/models/py_models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
from typing import Annotated, Any, Callable, Literal

from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_validator
Expand Down Expand Up @@ -469,6 +470,18 @@ def rekey_selected_agencies(cls, value: Any) -> Any:
rekeyed[key] = agency
return rekeyed

@field_validator("timePeriodFY")
@classmethod
def validate_time_period_fy(cls, value: list[str]) -> list[str]:
"""Fiscal years must be bare four-digit strings (e.g. '2026'), not 'FY2026'."""
invalid = [year for year in value if not re.fullmatch(r"\d{4}", year)]
if invalid:
raise ValueError(
f"Invalid fiscal year(s) in timePeriodFY: {invalid}. Fiscal years must be four-digit strings "
"(e.g. '2026'), without a 'FY' prefix."
)
return value

@field_validator("awardType")
@classmethod
def validate_award_type(cls, value: list[str]) -> list[str]:
Expand Down
43 changes: 43 additions & 0 deletions usaspending_api/llm/tests/integration/test_py_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,49 @@ def test_recipient_type_accepts_all_valid_types(self):
assert len(filters.recipientType) == len(all_types)


class TestTimePeriodFY:
"""Tests for the timePeriodFY four-digit-year validation on the Filters model"""

def test_time_period_fy_default_empty_list(self):
filters = Filters()

assert filters.timePeriodFY == []

def test_time_period_fy_accepts_four_digit_years(self):
filters = Filters(timePeriodFY=["2023", "2024"])

assert filters.timePeriodFY == ["2023", "2024"]

def test_time_period_fy_accepts_single_year(self):
filters = Filters(timePeriodFY=["2026"])

assert filters.timePeriodFY == ["2026"]

def test_time_period_fy_rejects_fy_prefix(self):
with pytest.raises(ValidationError) as exc_info:
Filters(timePeriodFY=["FY2026"])

assert "Invalid fiscal year(s)" in str(exc_info.value)

def test_time_period_fy_rejects_fy_prefix_mixed_with_valid_year(self):
with pytest.raises(ValidationError) as exc_info:
Filters(timePeriodFY=["2024", "FY2026"])

assert "FY2026" in str(exc_info.value)

def test_time_period_fy_rejects_non_four_digit_year(self):
with pytest.raises(ValidationError) as exc_info:
Filters(timePeriodFY=["26"])

assert "Invalid fiscal year(s)" in str(exc_info.value)

def test_time_period_fy_rejects_non_numeric_value(self):
with pytest.raises(ValidationError) as exc_info:
Filters(timePeriodFY=["this year"])

assert "Invalid fiscal year(s)" in str(exc_info.value)


class TestAwardType:
"""Tests for the awardType single-group validation on the Filters model"""

Expand Down
Loading