mirror of
https://github.com/Bunsly/HomeHarvest.git
synced 2026-03-04 19:44:29 -08:00
Add last_update_date filtering and improve time interface DX
Part A: Add last_update_date filtering (client-side) - Add updated_since parameter (accepts datetime object or ISO string) - Add updated_in_past_hours parameter (accepts int or timedelta) - Implement _apply_last_update_date_filter() method for client-side filtering - Add mutual exclusion validation for updated_* parameters Part B: Improve time interface DX - Accept datetime/timedelta objects for datetime_from, datetime_to - Accept timedelta objects for past_hours, past_days - Add type conversion helper functions in utils.py - Improve validation error messages with specific examples - Update validate_datetime to accept datetime objects Helper functions added: - convert_to_datetime_string() - Converts datetime objects to ISO strings - extract_timedelta_hours() - Extracts hours from timedelta objects - extract_timedelta_days() - Extracts days from timedelta objects - validate_last_update_filters() - Validates last_update_date parameters All changes are backward compatible - existing string/int parameters still work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -172,7 +172,7 @@ def validate_input(listing_type: str | list[str] | None) -> None:
|
||||
|
||||
def validate_dates(date_from: str | None, date_to: str | None) -> None:
|
||||
if isinstance(date_from, str) != isinstance(date_to, str):
|
||||
raise InvalidDate("Both date_from and date_to must be provided.")
|
||||
raise InvalidDate("Both date_from and date_to must be provided together.")
|
||||
|
||||
if date_from and date_to:
|
||||
try:
|
||||
@@ -180,9 +180,16 @@ def validate_dates(date_from: str | None, date_to: str | None) -> None:
|
||||
date_to_obj = datetime.strptime(date_to, "%Y-%m-%d")
|
||||
|
||||
if date_to_obj < date_from_obj:
|
||||
raise InvalidDate("date_to must be after date_from.")
|
||||
except ValueError:
|
||||
raise InvalidDate(f"Invalid date format or range")
|
||||
raise InvalidDate(f"date_to ('{date_to}') must be after date_from ('{date_from}').")
|
||||
except ValueError as e:
|
||||
# Provide specific guidance on the expected format
|
||||
if "does not match format" in str(e):
|
||||
raise InvalidDate(
|
||||
f"Invalid date format. Expected 'YYYY-MM-DD' format. "
|
||||
f"Examples: '2025-01-20', '2024-12-31'. "
|
||||
f"Got: date_from='{date_from}', date_to='{date_to}'"
|
||||
)
|
||||
raise InvalidDate(f"Invalid date format or range: {e}")
|
||||
|
||||
|
||||
def validate_limit(limit: int) -> None:
|
||||
@@ -222,21 +229,53 @@ def validate_offset(offset: int, limit: int = 10000) -> None:
|
||||
)
|
||||
|
||||
|
||||
def validate_datetime(datetime_str: str | None) -> None:
|
||||
"""Validate ISO 8601 datetime format."""
|
||||
if not datetime_str:
|
||||
def validate_datetime(datetime_value) -> None:
|
||||
"""Validate datetime value (accepts datetime objects or ISO 8601 strings)."""
|
||||
if datetime_value is None:
|
||||
return
|
||||
|
||||
# Already a datetime object - valid
|
||||
from datetime import datetime as dt, date
|
||||
if isinstance(datetime_value, (dt, date)):
|
||||
return
|
||||
|
||||
# Must be a string - validate ISO 8601 format
|
||||
if not isinstance(datetime_value, str):
|
||||
raise InvalidDate(
|
||||
f"Invalid datetime value. Expected datetime object, date object, or ISO 8601 string. "
|
||||
f"Got: {type(datetime_value).__name__}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Try parsing as ISO 8601 datetime
|
||||
datetime.fromisoformat(datetime_str.replace('Z', '+00:00'))
|
||||
datetime.fromisoformat(datetime_value.replace('Z', '+00:00'))
|
||||
except (ValueError, AttributeError):
|
||||
raise InvalidDate(
|
||||
f"Invalid datetime format: '{datetime_str}'. "
|
||||
f"Invalid datetime format: '{datetime_value}'. "
|
||||
f"Expected ISO 8601 format (e.g., '2025-01-20T14:30:00' or '2025-01-20')."
|
||||
)
|
||||
|
||||
|
||||
def validate_last_update_filters(updated_since: str | None, updated_in_past_hours: int | None) -> None:
|
||||
"""Validate last_update_date filtering parameters."""
|
||||
if updated_since and updated_in_past_hours:
|
||||
raise ValueError(
|
||||
"Cannot use both 'updated_since' and 'updated_in_past_hours' parameters together. "
|
||||
"Please use only one method to filter by last_update_date."
|
||||
)
|
||||
|
||||
# Validate updated_since format if provided
|
||||
if updated_since:
|
||||
validate_datetime(updated_since)
|
||||
|
||||
# Validate updated_in_past_hours range if provided
|
||||
if updated_in_past_hours is not None:
|
||||
if updated_in_past_hours < 1:
|
||||
raise ValueError(
|
||||
f"updated_in_past_hours must be at least 1. Got: {updated_in_past_hours}"
|
||||
)
|
||||
|
||||
|
||||
def validate_filters(
|
||||
beds_min: int | None = None,
|
||||
beds_max: int | None = None,
|
||||
@@ -282,3 +321,95 @@ def validate_sort(sort_by: str | None, sort_direction: str | None = "desc") -> N
|
||||
f"Invalid sort_direction value: '{sort_direction}'. "
|
||||
f"Valid options: {', '.join(valid_directions)}"
|
||||
)
|
||||
|
||||
|
||||
def convert_to_datetime_string(value) -> str | None:
|
||||
"""
|
||||
Convert datetime object or string to ISO 8601 string format.
|
||||
|
||||
Accepts:
|
||||
- datetime.datetime objects
|
||||
- datetime.date objects
|
||||
- ISO 8601 strings (returned as-is)
|
||||
- None (returns None)
|
||||
|
||||
Returns ISO 8601 formatted string or None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Already a string - return as-is
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
|
||||
# datetime.datetime object
|
||||
from datetime import datetime, date
|
||||
if isinstance(value, datetime):
|
||||
return value.isoformat()
|
||||
|
||||
# datetime.date object (convert to datetime at midnight)
|
||||
if isinstance(value, date):
|
||||
return datetime.combine(value, datetime.min.time()).isoformat()
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid datetime value. Expected datetime object, date object, or ISO 8601 string. "
|
||||
f"Got: {type(value).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def extract_timedelta_hours(value) -> int | None:
|
||||
"""
|
||||
Extract hours from int or timedelta object.
|
||||
|
||||
Accepts:
|
||||
- int (returned as-is)
|
||||
- timedelta objects (converted to total hours)
|
||||
- None (returns None)
|
||||
|
||||
Returns integer hours or None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Already an int - return as-is
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
# timedelta object - convert to hours
|
||||
from datetime import timedelta
|
||||
if isinstance(value, timedelta):
|
||||
return int(value.total_seconds() / 3600)
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid past_hours value. Expected int or timedelta object. "
|
||||
f"Got: {type(value).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def extract_timedelta_days(value) -> int | None:
|
||||
"""
|
||||
Extract days from int or timedelta object.
|
||||
|
||||
Accepts:
|
||||
- int (returned as-is)
|
||||
- timedelta objects (converted to total days)
|
||||
- None (returns None)
|
||||
|
||||
Returns integer days or None.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
# Already an int - return as-is
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
|
||||
# timedelta object - convert to days
|
||||
from datetime import timedelta
|
||||
if isinstance(value, timedelta):
|
||||
return int(value.total_seconds() / 86400) # 86400 seconds in a day
|
||||
|
||||
raise ValueError(
|
||||
f"Invalid past_days value. Expected int or timedelta object. "
|
||||
f"Got: {type(value).__name__}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user