diff --git a/agenda/busy.py b/agenda/busy.py index 7dc744c..582bbce 100644 --- a/agenda/busy.py +++ b/agenda/busy.py @@ -2,7 +2,7 @@ import itertools import typing -from datetime import date, datetime, timedelta, timezone +from datetime import date, datetime, timedelta import flask import pycountry @@ -84,20 +84,13 @@ def get_busy_events( def _parse_datetime_field(datetime_obj: datetime | date | str) -> tuple[datetime, date]: """Parse a datetime field that could be datetime object or string.""" if isinstance(datetime_obj, datetime): - dt = datetime_obj - elif isinstance(datetime_obj, date): - dt = datetime.combine(datetime_obj, datetime.min.time(), tzinfo=timezone.utc) - elif isinstance(datetime_obj, str): + return datetime_obj, datetime_obj.date() + if isinstance(datetime_obj, date): + return datetime.combine(datetime_obj, datetime.min.time()), datetime_obj + if isinstance(datetime_obj, str): dt = datetime.fromisoformat(datetime_obj.replace("Z", "+00:00")) - else: - raise ValueError(f"Invalid datetime format: {datetime_obj}") - - if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) - else: - dt = dt.astimezone(timezone.utc) - - return dt, dt.date() + return dt, dt.date() + raise ValueError(f"Invalid datetime format: {datetime_obj}") def _get_accommodation_location( diff --git a/agenda/thespacedevs.py b/agenda/thespacedevs.py index 648d4ea..5b5cbf9 100644 --- a/agenda/thespacedevs.py +++ b/agenda/thespacedevs.py @@ -322,18 +322,6 @@ def summarize_launch(launch: Launch) -> Summary: } -def is_launches_cache_fresh(rocket_dir: str) -> bool: - """Return True if the launches cache is younger than the TTL.""" - now = datetime.now() - existing = [ - x for x in (filename_timestamp(f, "json") for f in os.listdir(rocket_dir)) if x - ] - if not existing: - return False - existing.sort(reverse=True) - return (now - existing[0][0]).total_seconds() <= ttl - - def load_cached_launches(rocket_dir: str) -> StrDict | None: """Read the most recent cache of launches.""" filename = get_most_recent_file(rocket_dir, "json") diff --git a/templates/trip_page.html b/templates/trip_page.html index 6b51dee..0e3f80c 100644 --- a/templates/trip_page.html +++ b/templates/trip_page.html @@ -281,9 +281,7 @@ {% set item = e.detail %} {% set full_flight_number = item.airline_code + item.flight_number %} {% set radarbox_url = "https://www.radarbox.com/data/flights/" + full_flight_number %} - {% set depart_date = item.depart.date() if item.depart.hour is defined else item.depart %} - {% set arrive_date = item.arrive.date() if (item.arrive and item.arrive.hour is defined) else item.arrive %} - {% set is_overnight = item.arrive and depart_date != arrive_date %} + {% set is_overnight = item.arrive and item.depart.date() != item.arrive.date() %}
@@ -319,9 +317,7 @@ {% elif e.element_type == "train" %} {% set item = e.detail %} - {% set depart_date = item.depart.date() if item.depart.hour is defined else item.depart %} - {% set arrive_date = item.arrive.date() if item.arrive.hour is defined else item.arrive %} - {% set is_overnight = depart_date != arrive_date %} + {% set is_overnight = item.depart.date() != item.arrive.date() %}
diff --git a/tests/test_busy.py b/tests/test_busy.py index d3b80fc..8a12d8c 100644 --- a/tests/test_busy.py +++ b/tests/test_busy.py @@ -1,4 +1,4 @@ -from datetime import date, datetime, timezone +from datetime import date, datetime import agenda.busy import agenda.travel as travel @@ -157,8 +157,7 @@ def test_parse_datetime_field(): # Test with datetime object dt = datetime(2023, 1, 1, 12, 0, 0) parsed_dt, parsed_date = _parse_datetime_field(dt) - assert parsed_dt == dt.replace(tzinfo=timezone.utc) - assert parsed_dt.tzinfo == timezone.utc + assert parsed_dt == dt assert parsed_date == date(2023, 1, 1) # Test with ISO string diff --git a/tests/test_busy_timezone.py b/tests/test_busy_timezone.py deleted file mode 100644 index 73d35dd..0000000 --- a/tests/test_busy_timezone.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Regression tests for timezone handling in busy location logic.""" - -from datetime import date, datetime, timezone - -import agenda.busy -from agenda.types import Trip - - -def test_mixed_naive_and_aware_arrivals_do_not_crash() -> None: - """Most recent travel should compare mixed timezone styles safely.""" - trips = [ - Trip( - start=date(2099, 12, 30), - travel=[ - { - "type": "flight", - "arrive": datetime(2100, 1, 1, 10, 0, 0), - "to": "CDG", - "to_airport": {"country": "fr", "city": "Paris"}, - }, - { - "type": "flight", - "arrive": datetime(2100, 1, 1, 12, 0, 0, tzinfo=timezone.utc), - "to": "AMS", - "to_airport": {"country": "nl", "city": "Amsterdam"}, - }, - ], - ) - ] - - location = agenda.busy._find_most_recent_travel_before_date(date(2100, 1, 1), trips) - assert location is not None - assert location[0] == "Amsterdam" - assert location[1] is not None - assert location[1].alpha_2 == "NL" diff --git a/tests/test_trip.py b/tests/test_trip.py deleted file mode 100644 index 69c0e6e..0000000 --- a/tests/test_trip.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Tests for trip map coordinate assembly.""" - -from datetime import date - -import agenda.trip -from agenda.types import Trip -from web_view import app - - -def test_add_coordinates_for_unbooked_flights_adds_missing_airports() -> None: - """Unbooked routes should contribute missing airport pins.""" - routes = [ - { - "type": "unbooked_flight", - "key": "LHR_Paris_fr", - "from_iata": "LHR", - "to_iata": "CDG", - "from": (51.47, -0.45), - "to": (49.01, 2.55), - } - ] - coordinates = [ - { - "name": "Heathrow Airport", - "type": "airport", - "latitude": 51.47, - "longitude": -0.45, - } - ] - airports = { - "LHR": { - "name": "Heathrow Airport", - "latitude": 51.47, - "longitude": -0.45, - }, - "CDG": { - "name": "Paris Charles de Gaulle Airport", - "latitude": 49.01, - "longitude": 2.55, - }, - } - - with app.app_context(): - original_parse_yaml = agenda.trip.travel.parse_yaml - try: - agenda.trip.travel.parse_yaml = lambda _name, _data_dir: airports - agenda.trip.add_coordinates_for_unbooked_flights( - routes, coordinates, app.config["PERSONAL_DATA"] - ) - finally: - agenda.trip.travel.parse_yaml = original_parse_yaml - - airport_names = { - coord["name"] for coord in coordinates if coord["type"] == "airport" - } - assert airport_names == {"Heathrow Airport", "Paris Charles de Gaulle Airport"} - - -def test_get_coordinates_and_routes_adds_unbooked_flight_airports() -> None: - """Trip list map data should include pins for unbooked flights.""" - trips = [Trip(start=date(2026, 7, 20))] - unbooked_routes = [ - { - "type": "unbooked_flight", - "key": "LHR_Paris_fr", - "from_iata": "LHR", - "to_iata": "CDG", - "from": (51.47, -0.45), - "to": (49.01, 2.55), - } - ] - airports = { - "LHR": { - "name": "Heathrow Airport", - "latitude": 51.47, - "longitude": -0.45, - }, - "CDG": { - "name": "Paris Charles de Gaulle Airport", - "latitude": 49.01, - "longitude": 2.55, - }, - } - - with app.app_context(): - original_collect_trip_coordinates = agenda.trip.collect_trip_coordinates - original_get_trip_routes = agenda.trip.get_trip_routes - original_parse_yaml = agenda.trip.travel.parse_yaml - try: - agenda.trip.collect_trip_coordinates = lambda _trip: [] - agenda.trip.get_trip_routes = lambda _trip, _data_dir: unbooked_routes - agenda.trip.travel.parse_yaml = lambda _name, _data_dir: airports - - coordinates, _routes = agenda.trip.get_coordinates_and_routes(trips) - finally: - agenda.trip.collect_trip_coordinates = original_collect_trip_coordinates - agenda.trip.get_trip_routes = original_get_trip_routes - agenda.trip.travel.parse_yaml = original_parse_yaml - - airport_names = { - coord["name"] for coord in coordinates if coord["type"] == "airport" - } - assert airport_names == {"Heathrow Airport", "Paris Charles de Gaulle Airport"} diff --git a/tests/test_trip_page_route.py b/tests/test_trip_page_route.py deleted file mode 100644 index d471caa..0000000 --- a/tests/test_trip_page_route.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Regression tests for trip page route wiring and rendering.""" - -from datetime import date -import typing - -import web_view -from agenda.types import Trip - - -def test_trip_page_passes_data_dir_to_unbooked_flight_helper() -> None: - """Trip page should call helper with routes, coordinates and data_dir.""" - trip = Trip(start=date(2025, 1, 28)) - captured: dict[str, str] = {} - - with web_view.app.app_context(): - original_get_trip_list = web_view.get_trip_list - original_add_schengen = ( - web_view.agenda.trip_schengen.add_schengen_compliance_to_trip - ) - original_collect_trip_coordinates = ( - web_view.agenda.trip.collect_trip_coordinates - ) - original_get_trip_routes = web_view.agenda.trip.get_trip_routes - original_add_coordinates = ( - web_view.agenda.trip.add_coordinates_for_unbooked_flights - ) - original_get_trip_weather = web_view.agenda.weather.get_trip_weather - original_render_template = web_view.flask.render_template - try: - web_view.get_trip_list = lambda: [trip] - web_view.agenda.trip_schengen.add_schengen_compliance_to_trip = lambda t: t - web_view.agenda.trip.collect_trip_coordinates = lambda _trip: [] - web_view.agenda.trip.get_trip_routes = lambda _trip, _data_dir: [] - - def fake_add_coordinates( - _routes: list[typing.Any], - _coordinates: list[typing.Any], - data_dir: str, - ) -> None: - captured["data_dir"] = data_dir - - web_view.agenda.trip.add_coordinates_for_unbooked_flights = ( - fake_add_coordinates - ) - web_view.agenda.weather.get_trip_weather = lambda *_args, **_kwargs: [] - web_view.flask.render_template = lambda *_args, **_kwargs: "ok" - - with web_view.app.test_request_context("/trip/2025-01-28"): - result = web_view.trip_page("2025-01-28") - - assert result == "ok" - assert captured["data_dir"] == web_view.app.config["PERSONAL_DATA"] - finally: - web_view.get_trip_list = original_get_trip_list - web_view.agenda.trip_schengen.add_schengen_compliance_to_trip = ( - original_add_schengen - ) - web_view.agenda.trip.collect_trip_coordinates = ( - original_collect_trip_coordinates - ) - web_view.agenda.trip.get_trip_routes = original_get_trip_routes - web_view.agenda.trip.add_coordinates_for_unbooked_flights = ( - original_add_coordinates - ) - web_view.agenda.weather.get_trip_weather = original_get_trip_weather - web_view.flask.render_template = original_render_template - - -def test_trip_page_renders_with_date_only_train_leg() -> None: - """Trip page should render when train legs use date values (no time).""" - trip = Trip( - start=date(2025, 1, 28), - travel=[ - { - "type": "train", - "depart": date(2025, 1, 28), - "from": "A", - "to": "B", - "from_station": { - "name": "A", - "country": "gb", - "latitude": 51.5, - "longitude": -0.1, - }, - "to_station": { - "name": "B", - "country": "gb", - "latitude": 51.6, - "longitude": -0.2, - }, - "legs": [ - { - "from": "A", - "to": "B", - "depart": date(2025, 1, 28), - "arrive": date(2025, 1, 28), - "from_station": { - "name": "A", - "country": "gb", - "latitude": 51.5, - "longitude": -0.1, - }, - "to_station": { - "name": "B", - "country": "gb", - "latitude": 51.6, - "longitude": -0.2, - }, - "operator": "Test Rail", - } - ], - } - ], - ) - - with web_view.app.app_context(): - original_get_trip_list = web_view.get_trip_list - original_get_trip_weather = web_view.agenda.weather.get_trip_weather - try: - web_view.get_trip_list = lambda: [trip] - web_view.agenda.weather.get_trip_weather = lambda *_args, **_kwargs: [] - web_view.app.config["TESTING"] = True - - with web_view.app.test_client() as client: - response = client.get("/trip/2025-01-28") - - assert response.status_code == 200 - assert b"Test Rail" in response.data - finally: - web_view.get_trip_list = original_get_trip_list - web_view.agenda.weather.get_trip_weather = original_get_trip_weather diff --git a/tests/test_weekends_route.py b/tests/test_weekends_route.py deleted file mode 100644 index 46de40c..0000000 --- a/tests/test_weekends_route.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Tests for weekends route query validation.""" - -from datetime import date -import typing - -import pytest - -import web_view - - -@pytest.fixture # type: ignore[untyped-decorator] -def client() -> typing.Any: - """Flask test client.""" - web_view.app.config["TESTING"] = True - with web_view.app.test_client() as c: - yield c - - -def test_weekends_rejects_year_before_2020(client: typing.Any) -> None: - """Years before 2020 should return HTTP 400.""" - response = client.get("/weekends?year=2019&week=1") - assert response.status_code == 400 - assert b"Year must be between 2020" in response.data - - -def test_weekends_rejects_year_more_than_five_years_ahead(client: typing.Any) -> None: - """Years beyond current year + 5 should return HTTP 400.""" - too_far = date.today().year + 6 - response = client.get(f"/weekends?year={too_far}&week=1") - assert response.status_code == 400 - assert b"Year must be between 2020" in response.data diff --git a/update.py b/update.py index f6e5ef3..97e4d41 100755 --- a/update.py +++ b/update.py @@ -321,9 +321,6 @@ def update_thespacedevs(config: flask.config.Config) -> None: existing_data = agenda.thespacedevs.load_cached_launches(rocket_dir) assert existing_data - if agenda.thespacedevs.is_launches_cache_fresh(rocket_dir): - return - # Update active crewed mission cache used by the launches page. # Uses the 2-hour TTL; failures are handled internally with cache fallback. active_crewed = agenda.thespacedevs.get_active_crewed_flights(rocket_dir) diff --git a/web_view.py b/web_view.py index 18a62dc..4fa2d7c 100755 --- a/web_view.py +++ b/web_view.py @@ -268,16 +268,6 @@ async def gaps_page() -> str: async def weekends() -> str: """List of available weekends using an optional date, week, or year parameter.""" today = datetime.now().date() - min_year = 2020 - max_year = today.year + 5 - - def validate_year(year: int) -> None: - """Validate year parameter range for weekends page.""" - if year < min_year or year > max_year: - flask.abort( - 400, description=f"Year must be between {min_year} and {max_year}." - ) - date_str = flask.request.args.get("date") week_str = flask.request.args.get("week") year_str = flask.request.args.get("year") @@ -285,14 +275,12 @@ async def weekends() -> str: if date_str: try: start = datetime.strptime(date_str, "%Y-%m-%d").date() - validate_year(start.year) except ValueError: return flask.abort(400, description="Invalid date format. Use YYYY-MM-DD.") elif week_str: try: week = int(week_str) year = int(year_str) if year_str else today.year - validate_year(year) if week < 1 or week > 53: return flask.abort( 400, description="Week number must be between 1 and 53." @@ -305,13 +293,6 @@ async def weekends() -> str: return flask.abort( 400, description="Invalid week or year format. Use integers." ) - elif year_str: - try: - year = int(year_str) - validate_year(year) - start = date(year, 1, 1) - except ValueError: - return flask.abort(400, description="Invalid year format. Use an integer.") else: start = date(today.year, 1, 1) @@ -933,7 +914,9 @@ def get_destination_timezones(trip: Trip) -> list[StrDict]: if flight_country: flight_locations.append((city, flight_country)) - existing_location_keys = {(loc, c.alpha_2.lower()) for loc, c in trip.locations()} + existing_location_keys = { + (loc, c.alpha_2.lower()) for loc, c in trip.locations() + } all_locations = list(trip.locations()) + [ (city, country) for city, country in flight_locations @@ -1035,9 +1018,7 @@ def trip_page(start: str) -> str: coordinates = agenda.trip.collect_trip_coordinates(trip) routes = agenda.trip.get_trip_routes(trip, app.config["PERSONAL_DATA"]) - agenda.trip.add_coordinates_for_unbooked_flights( - routes, coordinates, app.config["PERSONAL_DATA"] - ) + agenda.trip.add_coordinates_for_unbooked_flights(routes, coordinates) for route in routes: if "geojson_filename" in route: