diff --git a/tests/test_conference.py b/tests/test_conference.py deleted file mode 100644 index 811bdb9..0000000 --- a/tests/test_conference.py +++ /dev/null @@ -1,370 +0,0 @@ -"""Tests for agenda.conference module.""" - -import decimal -import tempfile -from datetime import date, datetime -from typing import Any - -import pytest -import yaml - -from agenda.conference import Conference, get_list -from agenda.event import Event - - -class TestConference: - """Tests for Conference dataclass.""" - - def test_conference_creation_minimal(self) -> None: - """Test creating conference with minimal required fields.""" - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.name == "PyCon" - assert conf.topic == "Python" - assert conf.location == "Portland" - assert conf.start == date(2024, 5, 15) - assert conf.end == date(2024, 5, 17) - assert conf.trip is None - assert conf.going is False - assert conf.online is False - - def test_conference_creation_full(self) -> None: - """Test creating conference with all fields.""" - conf = Conference( - name="PyCon US", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - trip=date(2024, 5, 14), - country="USA", - venue="Convention Center", - address="123 Main St", - url="https://pycon.org", - accommodation_booked=True, - transport_booked=True, - going=True, - registered=True, - speaking=True, - online=False, - price=decimal.Decimal("500.00"), - currency="USD", - latitude=45.5152, - longitude=-122.6784, - cfp_end=date(2024, 2, 1), - cfp_url="https://pycon.org/cfp", - free=False, - hackathon=True, - ticket_type="early_bird", - attendees=3000, - hashtag="#pycon2024", - ) - assert conf.name == "PyCon US" - assert conf.going is True - assert conf.price == decimal.Decimal("500.00") - assert conf.currency == "USD" - assert conf.latitude == 45.5152 - assert conf.longitude == -122.6784 - assert conf.cfp_end == date(2024, 2, 1) - assert conf.hashtag == "#pycon2024" - - def test_display_name_location_in_name(self) -> None: - """Test display_name when location is already in conference name.""" - conf = Conference( - name="PyCon Portland", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyCon Portland" - - def test_display_name_location_not_in_name(self) -> None: - """Test display_name when location is not in conference name.""" - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyCon (Portland)" - - def test_display_name_partial_location_match(self) -> None: - """Test display_name when location is partially in name.""" - conf = Conference( - name="PyConf", - topic="Python", - location="Conference Center", - start=date(2024, 5, 15), - end=date(2024, 5, 17), - ) - assert conf.display_name == "PyConf (Conference Center)" - - def test_conference_with_datetime(self) -> None: - """Test conference with datetime objects.""" - start_dt = datetime(2024, 5, 15, 9, 0) - end_dt = datetime(2024, 5, 17, 17, 0) - conf = Conference( - name="PyCon", - topic="Python", - location="Portland", - start=start_dt, - end=end_dt, - ) - assert conf.start == start_dt - assert conf.end == end_dt - - -class TestGetList: - """Tests for get_list function.""" - - def test_get_list_single_conference(self) -> None: - """Test reading single conference from YAML.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "going": True, - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert isinstance(event, Event) - assert event.name == "conference" - assert event.date == date(2024, 5, 15) - assert event.end_date == date(2024, 5, 17) - assert event.title == "PyCon (Portland)" - assert event.url == "https://pycon.org" - assert event.going is True - - def test_get_list_conference_with_cfp(self) -> None: - """Test reading conference with CFP end date.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "cfp_end": date(2024, 2, 1), - "cfp_url": "https://pycon.org/cfp", - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 2 - - # Conference event - conf_event = events[0] - assert conf_event.name == "conference" - assert conf_event.title == "PyCon (Portland)" - assert conf_event.url == "https://pycon.org" - - # CFP end event - cfp_event = events[1] - assert cfp_event.name == "cfp_end" - assert cfp_event.date == date(2024, 2, 1) - assert cfp_event.title == "CFP end: PyCon (Portland)" - assert cfp_event.url == "https://pycon.org/cfp" - - def test_get_list_conference_cfp_no_url(self) -> None: - """Test reading conference with CFP end date but no CFP URL.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - "url": "https://pycon.org", - "cfp_end": date(2024, 2, 1), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 2 - cfp_event = events[1] - assert cfp_event.url == "https://pycon.org" # Falls back to conference URL - - def test_get_list_multiple_conferences(self) -> None: - """Test reading multiple conferences from YAML.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - }, - { - "name": "EuroPython", - "topic": "Python", - "location": "Prague", - "start": date(2024, 7, 8), - "end": date(2024, 7, 14), - "cfp_end": date(2024, 3, 15), - }, - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 3 # 2 conferences + 1 CFP end - - # First conference - assert events[0].title == "PyCon (Portland)" - assert events[0].date == date(2024, 5, 15) - - # Second conference - assert events[1].title == "EuroPython (Prague)" - assert events[1].date == date(2024, 7, 8) - - # CFP end event for second conference - assert events[2].name == "cfp_end" - assert events[2].title == "CFP end: EuroPython (Prague)" - - def test_get_list_location_in_name(self) -> None: - """Test conference where location is already in name.""" - yaml_data = [ - { - "name": "PyCon Portland", - "topic": "Python", - "location": "Portland", - "start": date(2024, 5, 15), - "end": date(2024, 5, 17), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - assert events[0].title == "PyCon Portland" # No location appended - - def test_get_list_datetime_objects(self) -> None: - """Test reading conferences with datetime objects.""" - yaml_data = [ - { - "name": "PyCon", - "topic": "Python", - "location": "Portland", - "start": datetime(2024, 5, 15, 9, 0), - "end": datetime(2024, 5, 17, 17, 0), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert event.date == datetime(2024, 5, 15, 9, 0) - assert event.end_date == datetime(2024, 5, 17, 17, 0) - - def test_get_list_invalid_date_order(self) -> None: - """Test that conferences with end before start raise assertion error.""" - yaml_data = [ - { - "name": "Invalid Conference", - "topic": "Testing", - "location": "Nowhere", - "start": date(2024, 5, 17), - "end": date(2024, 5, 15), # End before start - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - with pytest.raises(AssertionError): - get_list(f.name) - - def test_get_list_too_long_conference(self) -> None: - """Test that conferences longer than MAX_CONF_DAYS raise assertion error.""" - yaml_data = [ - { - "name": "Too Long Conference", - "topic": "Testing", - "location": "Nowhere", - "start": date(2024, 5, 1), - "end": date(2024, 6, 1), # More than 20 days - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - with pytest.raises(AssertionError): - get_list(f.name) - - def test_get_list_empty_file(self) -> None: - """Test reading empty YAML file.""" - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump([], f) - f.flush() - - events = get_list(f.name) - - assert events == [] - - def test_get_list_same_day_conference(self) -> None: - """Test conference that starts and ends on same day.""" - yaml_data = [ - { - "name": "One Day Conference", - "topic": "Testing", - "location": "Test City", - "start": date(2024, 5, 15), - "end": date(2024, 5, 15), - } - ] - - with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f: - yaml.dump(yaml_data, f) - f.flush() - - events = get_list(f.name) - - assert len(events) == 1 - event = events[0] - assert event.date == date(2024, 5, 15) - assert event.end_date == date(2024, 5, 15) diff --git a/tests/test_gwr.py b/tests/test_gwr.py deleted file mode 100644 index 03cd342..0000000 --- a/tests/test_gwr.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Tests for agenda.gwr module.""" - -import os -import tempfile -from datetime import date -from typing import Any -from unittest.mock import AsyncMock, patch - -import pytest - -from agenda.gwr import ( - advance_ticket_date, - advance_tickets_page_html, - extract_dates, - extract_weekday_date, - parse_date_string, -) - - -class TestParseDateString: - """Tests for parse_date_string function.""" - - def test_parse_date_with_year(self) -> None: - """Test parsing date string with year included.""" - result = parse_date_string("Monday 25 December 2023") - assert result == date(2023, 12, 25) - - def test_parse_date_without_year(self) -> None: - """Test parsing date string without year (should use current year).""" - current_year = date.today().year - result = parse_date_string("Monday 25 December") - assert result == date(current_year, 12, 25) - - def test_parse_different_formats(self) -> None: - """Test parsing various date formats.""" - result = parse_date_string("Friday 1 January 2024") - assert result == date(2024, 1, 1) - - result = parse_date_string("Saturday 29 February 2024") - assert result == date(2024, 2, 29) - - -class TestExtractDates: - """Tests for extract_dates function.""" - - def test_extract_valid_dates(self) -> None: - """Test extracting dates from valid HTML.""" - html = """ - WeekdaysMonday 25 December 2023 - SaturdaysSaturday 23 December 2023 - SundaysSunday 24 December 2023 - """ - result = extract_dates(html) - expected = { - "Weekdays": date(2023, 12, 25), - "Saturdays": date(2023, 12, 23), - "Sundays": date(2023, 12, 24), - } - assert result == expected - - def test_extract_dates_with_asterisks(self) -> None: - """Test extracting dates when HTML contains asterisks.""" - html = """ - WeekdaysMonday 25 December 2023** - """ - result = extract_dates(html) - expected = {"Weekdays": date(2023, 12, 25)} - assert result == expected - - def test_extract_dates_no_match(self) -> None: - """Test extracting dates when no pattern matches.""" - html = "

No relevant table data here

" - result = extract_dates(html) - assert result is None - - def test_extract_dates_whitespace_handling(self) -> None: - """Test extracting dates with various whitespace.""" - html = """ - Weekdays Monday 25 December 2023 - """ - result = extract_dates(html) - expected = {"Weekdays": date(2023, 12, 25)} - assert result == expected - - -class TestExtractWeekdayDate: - """Tests for extract_weekday_date function.""" - - def test_extract_valid_weekday_date(self) -> None: - """Test extracting weekday date from valid HTML.""" - html = """ - WeekdaysMonday 25 December 2023 - """ - result = extract_weekday_date(html) - assert result == date(2023, 12, 25) - - def test_extract_weekday_date_with_asterisks(self) -> None: - """Test extracting weekday date when HTML contains asterisks.""" - html = """ - WeekdaysMonday 25 December 2023** - """ - result = extract_weekday_date(html) - assert result == date(2023, 12, 25) - - def test_extract_weekday_date_no_match(self) -> None: - """Test extracting weekday date when no pattern matches.""" - html = "

No weekday data here

" - result = extract_weekday_date(html) - assert result is None - - def test_extract_weekday_date_multiline(self) -> None: - """Test extracting weekday date with multiline content.""" - html = """ - - Weekdays - Monday 25 December 2023 - - """ - result = extract_weekday_date(html) - assert result == date(2023, 12, 25) - - -class TestAdvanceTicketsPageHtml: - """Tests for advance_tickets_page_html function.""" - - @pytest.mark.asyncio - async def test_cache_hit(self) -> None: - """Test using cached HTML when file is fresh.""" - with tempfile.TemporaryDirectory() as temp_dir: - cache_file = os.path.join(temp_dir, "advance-tickets.html") - test_content = "test content" - - # Create a fresh cache file - with open(cache_file, "w") as f: - f.write(test_content) - - result = await advance_tickets_page_html(temp_dir, ttl=3600) - assert result == test_content - - @pytest.mark.asyncio - async def test_force_cache(self) -> None: - """Test forcing cache usage even when file doesn't exist.""" - with tempfile.TemporaryDirectory() as temp_dir: - cache_file = os.path.join(temp_dir, "advance-tickets.html") - test_content = "cached content" - - with open(cache_file, "w") as f: - f.write(test_content) - - result = await advance_tickets_page_html(temp_dir, force_cache=True) - assert result == test_content - - @pytest.mark.asyncio - @patch("httpx.AsyncClient") - async def test_fetch_from_web(self, mock_client: Any) -> None: - """Test fetching from web when cache is stale.""" - mock_response = AsyncMock() - mock_response.text = "fresh content" - mock_client.return_value.__aenter__.return_value.get.return_value = ( - mock_response - ) - - with tempfile.TemporaryDirectory() as temp_dir: - result = await advance_tickets_page_html(temp_dir, ttl=0) - assert result == "fresh content" - - # Check that content was cached - cache_file = os.path.join(temp_dir, "advance-tickets.html") - with open(cache_file) as f: - cached_content = f.read() - assert cached_content == "fresh content" - - -class TestAdvanceTicketDate: - """Tests for advance_ticket_date function.""" - - @pytest.mark.asyncio - @patch("agenda.gwr.advance_tickets_page_html") - async def test_advance_ticket_date_success(self, mock_html: Any) -> None: - """Test successfully extracting advance ticket date.""" - mock_html.return_value = """ - WeekdaysMonday 25 December 2023 - """ - - result = await advance_ticket_date("/fake/dir") - assert result == date(2023, 12, 25) - mock_html.assert_called_once_with("/fake/dir", force_cache=False) - - @pytest.mark.asyncio - @patch("agenda.gwr.advance_tickets_page_html") - async def test_advance_ticket_date_no_match(self, mock_html: Any) -> None: - """Test when no weekday date can be extracted.""" - mock_html.return_value = "

No relevant data

" - - result = await advance_ticket_date("/fake/dir") - assert result is None - - @pytest.mark.asyncio - @patch("agenda.gwr.advance_tickets_page_html") - async def test_advance_ticket_date_force_cache(self, mock_html: Any) -> None: - """Test advance_ticket_date with force_cache parameter.""" - mock_html.return_value = """ - WeekdaysTuesday 26 December 2023 - """ - - result = await advance_ticket_date("/fake/dir", force_cache=True) - assert result == date(2023, 12, 26) - mock_html.assert_called_once_with("/fake/dir", force_cache=True) diff --git a/validate_yaml.py b/validate_yaml.py index 6cf4028..ccbdd0d 100755 --- a/validate_yaml.py +++ b/validate_yaml.py @@ -4,7 +4,7 @@ import os import sys import typing -from datetime import date, timedelta, datetime +from datetime import date, timedelta import yaml from rich.pretty import pprint @@ -33,26 +33,7 @@ def check_currency(item: agenda.types.StrDict) -> None: def check_trips() -> None: - """Check trips and ensure they are in chronological order.""" - filepath = os.path.join(data_dir, "trips.yaml") - trips_data = yaml.safe_load(open(filepath, "r")) - - prev_trip = None - prev_trip_data = None - for trip_data in trips_data: - current_trip = normalize_datetime(trip_data["trip"]) - if prev_trip and current_trip < prev_trip: - print(f"Out of order trip found:") - print( - f" Previous: {prev_trip_data.get('trip')} - {prev_trip_data.get('name', 'No name')}" - ) - print( - f" Current: {trip_data.get('trip')} - {trip_data.get('name', 'No name')}" - ) - assert False, "Trips are not in chronological order by trip date." - prev_trip = current_trip - prev_trip_data = trip_data - + """Check trips.""" trip_list = agenda.trip.build_trip_list(data_dir) print(len(trip_list), "trips") @@ -95,68 +76,25 @@ def check_flights(airlines: set[str]) -> None: ) -def normalize_datetime(dt_value): - """Convert date or datetime to datetime for comparison, removing timezone info.""" - if isinstance(dt_value, date) and not isinstance(dt_value, datetime): - return datetime.combine(dt_value, datetime.min.time()) - elif isinstance(dt_value, datetime): - # Remove timezone info to allow comparison between naive and aware datetimes - return dt_value.replace(tzinfo=None) - return dt_value - - def check_trains() -> None: - """Check trains and ensure they are in chronological order.""" + """Check trains.""" trains = agenda.travel.parse_yaml("trains", data_dir) - - prev_depart = None - prev_train = None - for train in trains: - current_depart = normalize_datetime(train["depart"]) - if prev_depart and current_depart < prev_depart: - print(f"Out of order train found:") - print( - f" Previous: {prev_train.get('depart')} {prev_train.get('from', '')} -> {prev_train.get('to', '')}" - ) - print( - f" Current: {train.get('depart')} {train.get('from', '')} -> {train.get('to', '')}" - ) - assert False, "Trains are not in chronological order by departure time." - prev_depart = current_depart - prev_train = train - print(len(trains), "trains") def check_conferences() -> None: - """Check conferences and ensure they are in chronological order.""" + """Check conferences.""" filepath = os.path.join(data_dir, "conferences.yaml") - conferences_data = yaml.safe_load(open(filepath, "r")) - conferences = [agenda.conference.Conference(**conf) for conf in conferences_data] - - prev_start = None - prev_conf_data = None - for i, conf_data in enumerate(conferences_data): - conf = conferences[i] + conferences = [ + agenda.conference.Conference(**conf) + for conf in yaml.safe_load(open(filepath, "r")) + ] + for conf in conferences: if not conf.currency or conf.currency in currencies: - pass - else: - pprint(conf) - print(f"currency {conf.currency!r} not in {currencies!r}") - sys.exit(-1) - - current_start = normalize_datetime(conf_data["start"]) - if prev_start and current_start < prev_start: - print(f"Out of order conference found:") - print( - f" Previous: {prev_conf_data.get('start')} - {prev_conf_data.get('name', 'No name')}" - ) - print( - f" Current: {conf_data.get('start')} - {conf_data.get('name', 'No name')}" - ) - assert False, "Conferences are not in chronological order by start time." - prev_start = current_start - prev_conf_data = conf_data + continue + pprint(conf) + print(f"currency {conf.currency!r} not in {currencies!r}") + sys.exit(-1) print(len(conferences), "conferences") @@ -180,14 +118,12 @@ def check_coordinates(item: agenda.types.StrDict) -> None: def check_accommodation() -> None: - """Check accommodation and ensure they are in chronological order.""" + """Check accommodation.""" filepath = os.path.join(data_dir, "accommodation.yaml") accommodation_list = yaml.safe_load(open(filepath)) required_fields = ["type", "name", "country", "location", "trip", "from", "to"] - prev_from = None - prev_stay = None for stay in accommodation_list: try: assert all(field in stay for field in required_fields) @@ -198,21 +134,6 @@ def check_accommodation() -> None: check_currency(stay) - current_from = normalize_datetime(stay["from"]) - if prev_from and current_from < prev_from: - print(f"Out of order accommodation found:") - print( - f" Previous: {prev_stay.get('from')} - {prev_stay.get('name', 'No name')} ({prev_stay.get('location', '')})" - ) - print( - f" Current: {stay.get('from')} - {stay.get('name', 'No name')} ({stay.get('location', '')})" - ) - assert ( - False - ), "Accommodation is not in chronological order by check-in time." - prev_from = current_from - prev_stay = stay - print(len(accommodation_list), "stays") @@ -236,30 +157,6 @@ def check_stations() -> None: assert agenda.get_country(station["country"]) -def check_ferries() -> None: - """Check ferries and ensure they are in chronological order.""" - ferries = agenda.travel.parse_yaml("ferries", data_dir) - - prev_depart = None - prev_ferry = None - for ferry in ferries: - current_depart = normalize_datetime(ferry["depart"]) - if prev_depart and current_depart < prev_depart: - print(f"Out of order ferry found:") - print( - f" Previous: {prev_ferry.get('depart')} {prev_ferry.get('from', '')} -> {prev_ferry.get('to', '')}" - ) - print( - f" Current: {ferry.get('depart')} {ferry.get('from', '')} -> {ferry.get('to', '')}" - ) - assert False, "Ferries are not in chronological order by departure time." - prev_depart = current_depart - prev_ferry = ferry - check_currency(ferry) - - print(len(ferries), "ferries") - - def check_airlines() -> list[agenda.types.StrDict]: """Check airlines.""" airlines = agenda.travel.parse_yaml("airlines", data_dir) @@ -288,7 +185,6 @@ def check() -> None: check_trips() check_flights({airline["iata"] for airline in airlines}) check_trains() - check_ferries() check_conferences() check_events() check_accommodation()