diff --git a/integrations/aew.py b/integrations/aew.py new file mode 100644 index 0000000..bb7c9b7 --- /dev/null +++ b/integrations/aew.py @@ -0,0 +1,254 @@ +from typing import List +from datetime import datetime, timedelta +from zoneinfo import ZoneInfo +import re +import logging + +import requests +from bs4 import BeautifulSoup +from fastapi import HTTPException + +from base import CalendarBase, Event, IntegrationBase + +logger = logging.getLogger(__name__) + + +AEW_EVENTS_URL = "https://www.allelitewrestling.com/aew-events" + +# Timezone mappings for AEW events +TIMEZONE_MAP = { + "PT": "America/Los_Angeles", + "MT": "America/Denver", + "CT": "America/Chicago", + "ET": "America/New_York", + "EST": "America/New_York", + "AEDT": "Australia/Sydney", + "AEST": "Australia/Sydney", + "GMT": "Europe/London", + "BST": "Europe/London", +} + +# Month pattern for date matching +MONTHS = ["JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", + "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER"] + + +def parse_aew_datetime(date_str: str, time_str: str) -> datetime | None: + """ + Parse AEW date and time strings into a UTC datetime. + + Args: + date_str: e.g., "FEBRUARY 11, 2026" + time_str: e.g., "4:30pm PT" or "TBA" + + Returns: + datetime in UTC, or None if parsing fails + """ + try: + # Parse the date + date_str = date_str.strip().upper() + date = datetime.strptime(date_str, "%B %d, %Y") + + # Handle TBA times - default to 8 PM ET + if not time_str or time_str.upper() == "TBA": + time_str = "8:00pm ET" + + time_str = time_str.strip() + + # Extract timezone abbreviation from end of time string + parts = time_str.split() + if len(parts) >= 2: + tz_abbr = parts[-1].upper() + time_part = " ".join(parts[:-1]) + else: + tz_abbr = "ET" + time_part = time_str + + # Parse time (e.g., "4:30pm" or "8:00pm") + time_part = time_part.lower().replace(".", "") + if ":" in time_part: + time_obj = datetime.strptime(time_part, "%I:%M%p") + else: + time_obj = datetime.strptime(time_part, "%I%p") + + # Combine date and time + local_dt = datetime( + date.year, date.month, date.day, + time_obj.hour, time_obj.minute, 0 + ) + + # Convert to UTC + tz_name = TIMEZONE_MAP.get(tz_abbr, "America/New_York") + local_tz = ZoneInfo(tz_name) + local_dt = local_dt.replace(tzinfo=local_tz) + utc_dt = local_dt.astimezone(ZoneInfo("UTC")).replace(tzinfo=None) + + return utc_dt + + except Exception: + return None + + +def get_aew_events() -> List[dict]: + """ + Fetch all events from AEW.com events page. + + The AEW website is Wix-based and JavaScript-rendered. + We fetch the page and parse the text content which follows a pattern: + + DATE + EVENT NAME + City + EVENT DETAILS > + LOCATION + Venue + VENUE NAME + Time + TIME VALUE + BUY TICKETS + """ + try: + response = requests.get(AEW_EVENTS_URL, timeout=30, headers={ + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + }) + if response.status_code != 200: + return [] + + soup = BeautifulSoup(response.text, "html.parser") + + # Get all text content + text = soup.get_text(separator="\n") + lines = [line.strip() for line in text.split("\n") if line.strip()] + + events = [] + seen_uids = set() + + i = 0 + while i < len(lines): + line = lines[i] + + # Look for date pattern: MONTH DAY, YEAR + date_match = re.match(r"^(" + "|".join(MONTHS) + r")\s+\d{1,2},\s+\d{4}$", line.upper()) + + if date_match: + date_str = line + event_name = None + city = None + venue = None + time_str = None + + # Look ahead for event details (up to 15 lines or next date) + j = i + 1 + while j < min(i + 15, len(lines)): + next_line = lines[j] + + # Stop if we hit another date + if re.match(r"^(" + "|".join(MONTHS) + r")\s+\d{1,2},\s+\d{4}$", next_line.upper()): + break + + # Skip navigation/label lines + if next_line in ["City", "Venue", "Time", "EVENT DETAILS >", "BUY TICKETS", + "ON SALE: TBA", "Empty heading", "UPCOMING EVENTS"]: + j += 1 + continue + + # Event name contains "AEW" + if not event_name and "AEW" in next_line: + event_name = next_line + + # City/location pattern: "City, STATE" or "City, Country" + elif not city and re.match(r"^[A-Za-z\s]+,\s*[A-Za-z\s]+$", next_line) and len(next_line) < 50: + city = next_line + + # Venue names typically contain these words + elif not venue and any(v in next_line for v in + ["Arena", "Center", "Centre", "Stadium", "Auditorium", "Ballroom", + "Civic", "House", "Entertainment", "Bank"]): + venue = next_line + + # Time pattern: "H:MMpm TZ" or "H:MMam TZ" + elif not time_str and re.match(r"^\d{1,2}:\d{2}[ap]m\s+[A-Z]+$", next_line, re.I): + time_str = next_line + + j += 1 + + # Create event if we have minimum data + if event_name and date_str: + # Generate UID from event name and date + slug = re.sub(r"[^a-z0-9]+", "-", event_name.lower()).strip("-") + date_slug = re.sub(r"[^a-z0-9]+", "-", date_str.lower()).strip("-") + uid = f"aew-{slug}-{date_slug}" + + if uid not in seen_uids: + seen_uids.add(uid) + + # Parse datetime + start = parse_aew_datetime(date_str, time_str or "TBA") + if start: + location = "" + if venue and city: + location = f"{venue}, {city}" + elif city: + location = city + elif venue: + location = venue + + events.append({ + "uid": uid, + "title": event_name, + "start": start, + "location": location, + "description": "", + }) + + i = j # Skip to where we left off + else: + i += 1 + + return events + + except Exception as e: + logger.warning(f"AEW scraper error: {e}") + return [] + + +class AewCalendar(CalendarBase): + def fetch_events(self) -> List[Event]: + """Fetch AEW events from allelitewrestling.com.""" + try: + event_data = get_aew_events() + events: List[Event] = [] + + for data in event_data: + # AEW shows typically last 2-3 hours, PPVs 4+ hours + is_ppv = any(x in data["title"].lower() for x in + ["revolution", "dynasty", "double or nothing", "forbidden door", + "all in", "all out", "full gear", "worlds end", "grand slam"]) + duration = timedelta(hours=4) if is_ppv else timedelta(hours=3) + + events.append( + Event( + uid=data["uid"], + title=data["title"], + start=data["start"], + end=data["start"] + duration, + all_day=False, + description=data["description"], + location=data["location"], + ) + ) + + # Sort by start time + events.sort(key=lambda e: e.start) + self.events = events + return events + + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) from e + + +class AewIntegration(IntegrationBase): + def fetch_calendars(self, *args, **kwargs): + return None diff --git a/integrations/ufc.py b/integrations/ufc.py index e289da4..a1635c6 100644 --- a/integrations/ufc.py +++ b/integrations/ufc.py @@ -120,20 +120,66 @@ def get_event_details(event_url: str) -> dict | None: location = " ".join(venue_el.get_text().split()) break - # Build description with fight card + # Build description with fight card and broadcast info description_parts = [] - # Main card fights - main_card = soup.select_one("#main-card") - if main_card: - fights = main_card.select(".c-listing-fight") - if fights: - description_parts.append("Main Card:") - for fight in fights[:6]: # Limit to 6 fights - red_corner = fight.select_one(".c-listing-fight__corner--red .c-listing-fight__corner-name") - blue_corner = fight.select_one(".c-listing-fight__corner--blue .c-listing-fight__corner-name") - if red_corner and blue_corner: - description_parts.append(f" {red_corner.get_text(strip=True)} vs {blue_corner.get_text(strip=True)}") + # Find fight card sections (Main Card, Prelims, etc.) + broadcaster_containers = soup.select(".c-event-fight-card-broadcaster__container") + + for container in broadcaster_containers: + # Get card title (Main Card, Prelims, etc.) + card_title_el = container.select_one(".c-event-fight-card-broadcaster__card-title strong") + card_title = card_title_el.get_text(strip=True) if card_title_el else "Fight Card" + + # Get broadcaster/streaming info + broadcaster_link = container.select_one(".c-event-fight-card-broadcaster__link a") + broadcaster = "" + if broadcaster_link: + broadcaster = broadcaster_link.get_text(strip=True) + + # Get the fights for this card section + # The fights follow the broadcaster container in the next
+ next_section = container.find_next_sibling("section", class_="l-listing--stacked--full-width") + if not next_section: + # Try finding it as next element + next_section = container.find_next("section", class_="l-listing--stacked--full-width") + + fights_text = [] + if next_section: + fights = next_section.select(".c-listing-fight") + for fight in fights[:5]: # Limit to 5 fights per card + red_name = fight.select_one(".c-listing-fight__corner-name--red") + blue_name = fight.select_one(".c-listing-fight__corner-name--blue") + weight_class = fight.select_one(".c-listing-fight__class-text") + + if red_name and blue_name: + # Extract first and last names with proper spacing + red_given = red_name.select_one(".c-listing-fight__corner-given-name") + red_family = red_name.select_one(".c-listing-fight__corner-family-name") + blue_given = blue_name.select_one(".c-listing-fight__corner-given-name") + blue_family = blue_name.select_one(".c-listing-fight__corner-family-name") + + if red_given and red_family and blue_given and blue_family: + red = f"{red_given.get_text(strip=True)} {red_family.get_text(strip=True)}" + blue = f"{blue_given.get_text(strip=True)} {blue_family.get_text(strip=True)}" + else: + # Fallback to full text with space separator + red = " ".join(red_name.get_text().split()) + blue = " ".join(blue_name.get_text().split()) + + fight_str = f"• {red} vs {blue}" + if weight_class: + weight = weight_class.get_text(strip=True).replace(" Bout", "") + fight_str += f" ({weight})" + fights_text.append(fight_str) + + if fights_text: + header = card_title + if broadcaster: + header += f" - {broadcaster}" + description_parts.append(header) + description_parts.extend(fights_text) + description_parts.append("") # Empty line between sections # Generate a unique ID from the URL event_slug = event_url.split("/event/")[-1].split("?")[0] @@ -144,7 +190,7 @@ def get_event_details(event_url: str) -> dict | None: "title": title, "start": start_time, "location": location, - "description": "\n".join(description_parts) if description_parts else "", + "description": "\n".join(description_parts).strip() if description_parts else "", } except Exception: diff --git a/integrations/wwe.py b/integrations/wwe.py index 593e1a2..d27590a 100644 --- a/integrations/wwe.py +++ b/integrations/wwe.py @@ -1,99 +1,360 @@ -from typing import List +""" +WWE calendar integration. +Scrapes events from wwe.com/events with accurate local times. +""" +import logging +import re +from typing import List, Optional from datetime import datetime, timedelta +from zoneinfo import ZoneInfo import requests +from bs4 import BeautifulSoup from fastapi import HTTPException from base import CalendarBase, Event, IntegrationBase +logger = logging.getLogger(__name__) -API_URL = "https://www.wwe.com/api/events-search-results/all-events/all-dates/0/0/0/0" +# WWE events page URL - returns all events globally +EVENTS_URL = "https://www.wwe.com/events/results/all-events/all-dates/0/0/all/all" +# US state/region to timezone mapping +STATE_TIMEZONE_MAP = { + # Eastern Time + "NY": "America/New_York", "NJ": "America/New_York", "PA": "America/New_York", + "MA": "America/New_York", "CT": "America/New_York", "RI": "America/New_York", + "NH": "America/New_York", "VT": "America/New_York", "ME": "America/New_York", + "MD": "America/New_York", "DE": "America/New_York", "VA": "America/New_York", + "WV": "America/New_York", "NC": "America/New_York", "SC": "America/New_York", + "GA": "America/New_York", "FL": "America/New_York", "OH": "America/New_York", + "MI": "America/New_York", "KY": "America/New_York", + # Central Time + "IL": "America/Chicago", "WI": "America/Chicago", "MN": "America/Chicago", + "IA": "America/Chicago", "MO": "America/Chicago", "AR": "America/Chicago", + "LA": "America/Chicago", "MS": "America/Chicago", "AL": "America/Chicago", + "TN": "America/Chicago", "KS": "America/Chicago", "NE": "America/Chicago", + "SD": "America/Chicago", "ND": "America/Chicago", "OK": "America/Chicago", + "TX": "America/Chicago", "IN": "America/Chicago", + # Mountain Time + "MT": "America/Denver", "WY": "America/Denver", "CO": "America/Denver", + "NM": "America/Denver", "UT": "America/Denver", "ID": "America/Denver", + "AZ": "America/Phoenix", # Arizona doesn't observe DST + # Pacific Time + "WA": "America/Los_Angeles", "OR": "America/Los_Angeles", + "CA": "America/Los_Angeles", "NV": "America/Los_Angeles", + # Other US + "AK": "America/Anchorage", "HI": "Pacific/Honolulu", + # International (common WWE venues) + "UK": "Europe/London", "UK,": "Europe/London", + "QC": "America/Toronto", "ON": "America/Toronto", # Canada Eastern + "AB": "America/Edmonton", # Canada Mountain + "BC": "America/Vancouver", # Canada Pacific +} -def parse_wwe_datetime(date_str: str, time_str: str) -> datetime: - date_parts = date_str.split(", ") - if len(date_parts) != 2: - raise ValueError(f"Invalid date format: {date_str}") - month_day = date_parts[1] +# Country-level timezone defaults +COUNTRY_TIMEZONE_MAP = { + "Germany": "Europe/Berlin", + "UK": "Europe/London", + "England": "Europe/London", + "Scotland": "Europe/London", + "Ireland": "Europe/Dublin", + "Northern Ireland": "Europe/London", + "France": "Europe/Paris", + "Spain": "Europe/Madrid", + "Italy": "Europe/Rome", + "Saudi Arabia": "Asia/Riyadh", + "Australia": "Australia/Sydney", + "Japan": "Asia/Tokyo", + "Mexico": "America/Mexico_City", + "Canada": "America/Toronto", +} - time_parts = time_str.split(" ") - if len(time_parts) != 2: - raise ValueError(f"Invalid time format: {time_str}") - time_value = time_parts[0] - ampm = time_parts[1] - hour_minute = time_value.split(":") - if len(hour_minute) != 2: - raise ValueError(f"Invalid time format: {time_value}") - hour = int(hour_minute[0]) - minute = int(hour_minute[1]) +def get_timezone_for_location(city_state: str) -> ZoneInfo: + """ + Determine timezone from city/state string. + Examples: "Cleveland, OH", "London, UK", "Las Vegas, NV" + """ + # Try to extract state/region code + parts = city_state.split(",") + if len(parts) >= 2: + state_or_country = parts[-1].strip() - if ampm.upper() == "PM" and hour != 12: - hour += 12 - elif ampm.upper() == "AM" and hour == 12: - hour = 0 + # Check state mapping first + if state_or_country in STATE_TIMEZONE_MAP: + return ZoneInfo(STATE_TIMEZONE_MAP[state_or_country]) + + # Check country mapping + for country, tz in COUNTRY_TIMEZONE_MAP.items(): + if country.lower() in city_state.lower(): + return ZoneInfo(tz) + + # Default to US Eastern for unknown locations (most WWE events are US-based) + logger.warning(f"Unknown timezone for location: {city_state}, defaulting to America/New_York") + return ZoneInfo("America/New_York") + + +def parse_event_datetime(date_str: str, time_str: str, city_state: str) -> datetime: + """ + Parse date and time strings into UTC datetime. - current_year = datetime.now().year + Args: + date_str: e.g., "Feb 9" or "April 18th" + time_str: e.g., "7:30 PM" + city_state: e.g., "Cleveland, OH" - used to determine timezone + Returns: + datetime in UTC + """ month_map = { "Jan": 1, "Feb": 2, "Mar": 3, "Apr": 4, "May": 5, "Jun": 6, - "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12 + "Jul": 7, "Aug": 8, "Sep": 9, "Oct": 10, "Nov": 11, "Dec": 12, + "January": 1, "February": 2, "March": 3, "April": 4, + "May": 5, "June": 6, "July": 7, "August": 8, + "September": 9, "October": 10, "November": 11, "December": 12, } - month_day_parts = month_day.split(" ") - if len(month_day_parts) != 2: - raise ValueError(f"Invalid month/day format: {month_day}") - month_str = month_day_parts[0] - day = int(month_day_parts[1]) + + # Parse date: "Feb 9" or "April 18th" + date_str = date_str.strip() + # Remove ordinal suffixes (st, nd, rd, th) + date_str = re.sub(r'(\d+)(st|nd|rd|th)', r'\1', date_str) + + date_parts = date_str.split() + if len(date_parts) < 2: + raise ValueError(f"Invalid date format: {date_str}") + + month_str = date_parts[0] + day = int(date_parts[1]) + if month_str not in month_map: - raise ValueError(f"Invalid month: {month_str}") + raise ValueError(f"Unknown month: {month_str}") month = month_map[month_str] - return datetime(current_year, month, day, hour, minute) + # Determine year - if month is in the past, assume next year + current_date = datetime.now() + year = current_date.year + if month < current_date.month or (month == current_date.month and day < current_date.day): + year += 1 + + # Parse time: "7:30 PM" + time_str = time_str.strip().upper() + time_match = re.match(r'(\d{1,2}):(\d{2})\s*(AM|PM)', time_str) + if not time_match: + raise ValueError(f"Invalid time format: {time_str}") + hour = int(time_match.group(1)) + minute = int(time_match.group(2)) + ampm = time_match.group(3) -class WweCalendar(CalendarBase): - def fetch_events(self) -> List[Event]: + if ampm == "PM" and hour != 12: + hour += 12 + elif ampm == "AM" and hour == 12: + hour = 0 + + # Create local datetime with timezone + local_tz = get_timezone_for_location(city_state) + local_dt = datetime(year, month, day, hour, minute, tzinfo=local_tz) + + # Convert to UTC + utc_dt = local_dt.astimezone(ZoneInfo("UTC")) + + return utc_dt + + +# Show type classification keywords +SHOW_KEYWORDS = { + "raw": ["monday night raw", "raw"], + "smackdown": ["smackdown", "friday night smackdown"], + "nxt": ["nxt"], + "ple": ["wrestlemania", "summerslam", "royal rumble", "elimination chamber", + "survivor series", "money in the bank", "backlash", "clash", "bad blood", + "night of champions", "saturday night's main event"], +} + + +def classify_show(title: str) -> str: + """ + Classify an event into a show type based on its title. + + Returns one of: 'raw', 'smackdown', 'nxt', 'ple', 'other' + """ + title_lower = title.lower() + for show_type, keywords in SHOW_KEYWORDS.items(): + if any(kw in title_lower for kw in keywords): + return show_type + return "other" + + +def scrape_wwe_events(show: Optional[str] = None) -> List[dict]: + """ + Scrape WWE events from the official website. + + Args: + show: Optional filter for show type. One of: + - 'raw' - Monday Night RAW events + - 'smackdown' - Friday Night SmackDown events + - 'nxt' - NXT events + - 'ple' - Premium Live Events (WrestleMania, SummerSlam, etc.) + - 'all' or None - All events (default) + + Returns: + List of event dictionaries with title, start, end, venue, location, link + """ + headers = { + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + "AppleWebKit/537.36 (KHTML, like Gecko) " + "Chrome/120.0.0.0 Safari/537.36" + } + + response = requests.get(EVENTS_URL, headers=headers, timeout=30) + response.raise_for_status() + + soup = BeautifulSoup(response.text, "html.parser") + events = [] + seen_uids = set() # Track duplicates + + # Parse table rows containing events + # Format: "Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena" + for row in soup.select("tr"): try: - response = requests.get(API_URL, timeout=20) - if response.status_code != 200: - raise HTTPException(status_code=500, detail="Failed to fetch WWE events") - - data = response.json() - events: List[Event] = [] - for item in data: - if item.get("type") != "event": - continue - try: - start_time = parse_wwe_datetime(item["date"], item["time"]) - end_time = start_time + timedelta(hours=3) - # WWE site seems US-based; if you previously offset by +5 hours in routers, - # leave as-is or adjust here. We'll not add arbitrary offset here. - - events.append( - Event( - uid=f"wwe-{item['nid']}", - title=item["title"], - start=start_time, - end=end_time, - all_day=False, - description=f"WWE Event: {item.get('teaser_title', item['title'])}", - location=item.get('location', f"https://www.wwe.com{item['link']}") - if item.get('link') else item.get('location', ""), - ) - ) - except (ValueError, KeyError): + # Get title from link + title_el = row.select_one(".events-upcoming-header a") + if not title_el: + continue + + title = title_el.get_text(strip=True) + link = title_el.get("href", "") + if link and not link.startswith("http"): + link = f"https://www.wwe.com{link}" + + # Get event body text + body_el = row.select_one(".events-search-results--event-body") + if not body_el: + continue + + body_text = body_el.get_text(" ", strip=True) + + # Parse body text: "Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena" + # Pattern: DayOfWeek Month Day City, State Time | Venue + pattern = r"(\w+)\s+(\w+\s+\d+)\s+(.+?)\s+(\d{1,2}:\d{2}\s*[AP]M)\s*\|\s*(.+)" + match = re.match(pattern, body_text) + + if not match: + logger.debug(f"Could not parse event body: {body_text}") + continue + + day_of_week = match.group(1) + date_str = match.group(2) + city_state = match.group(3).strip() + time_str = match.group(4) + venue = match.group(5).strip() + + # Parse datetime + try: + start_utc = parse_event_datetime(date_str, time_str, city_state) + except ValueError as e: + logger.warning(f"Failed to parse datetime for {title}: {e}") + continue + + # Determine event duration based on type + if any(kw in title.lower() for kw in ["wrestlemania", "summerslam", "royal rumble", + "elimination chamber", "survivor series", + "money in the bank", "backlash"]): + duration_hours = 4 # Premium Live Events + elif "nxt" in title.lower(): + duration_hours = 2 # NXT shows + else: + duration_hours = 3 # RAW, SmackDown, house shows + + end_utc = start_utc + timedelta(hours=duration_hours) + + # Generate unique ID + uid = f"wwe-{title.lower().replace(' ', '-')}-{start_utc.strftime('%Y%m%d')}" + + # Skip duplicates (page sometimes shows events twice) + if uid in seen_uids: + continue + seen_uids.add(uid) + + # Classify show type + show_type = classify_show(title) + + # Apply show filter if specified + if show and show.lower() not in ["all", ""]: + if show_type != show.lower(): continue - self.events = events - return events - except HTTPException: - raise + events.append({ + "uid": uid, + "title": title, + "start": start_utc, + "end": end_utc, + "venue": venue, + "city_state": city_state, + "link": link, + "show_type": show_type, + }) + except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) from e + logger.warning(f"Error parsing event row: {e}") + continue + + logger.info(f"Scraped {len(events)} WWE events") + return events + + +class WweCalendar(CalendarBase): + """Calendar that fetches WWE events from wwe.com.""" + + def fetch_events(self, show: Optional[str] = None) -> List[Event]: + """ + Fetch upcoming WWE events. + + Args: + show: Optional filter for show type. One of: + - 'raw' - Monday Night RAW events only + - 'smackdown' - Friday Night SmackDown events only + - 'nxt' - NXT events only + - 'ple' - Premium Live Events only (WrestleMania, SummerSlam, etc.) + - 'all' or None - All events (default) + + Returns: + List of Event objects with event details. + """ + try: + event_data = scrape_wwe_events(show=show) + except requests.RequestException as e: + logger.warning(f"Failed to fetch WWE events: {e}") + raise HTTPException(status_code=502, detail=f"Failed to fetch WWE events: {e}") + + events = [] + for data in event_data: + location = f"{data['venue']}, {data['city_state']}" + + # Build description with event info + description_parts = [f"WWE Event: {data['title']}"] + if data.get("link"): + description_parts.append(f"More info: {data['link']}") + + event = Event( + uid=data["uid"], + title=data["title"], + start=data["start"], + end=data["end"], + all_day=False, + description="\n".join(description_parts), + location=location, + extra={}, + ) + events.append(event) + + self.events = events + return events class WweIntegration(IntegrationBase): + """Integration for WWE events.""" + def fetch_calendars(self, *args, **kwargs): return None - - diff --git a/main.py b/main.py index 2b99a68..8104d67 100644 --- a/main.py +++ b/main.py @@ -15,6 +15,7 @@ from integrations.thetvdb import TheTvDbIntegration, TheTvDbCalendar from integrations.wwe import WweIntegration, WweCalendar from integrations.ufc import UfcIntegration, UfcCalendar +from integrations.aew import AewIntegration, AewCalendar from integrations.shows import ShowsIntegration, ShowsCalendar from integrations.releases import ReleasesIntegration, ReleasesCalendar from integrations.sportsdb import SportsDbIntegration, SportsDbCalendar @@ -113,6 +114,14 @@ calendar_class=UfcCalendar, multi_calendar=False, ), + AewIntegration( + id="aew", + name="AEW", + description="AEW events scraped directly from allelitewrestling.com", + base_url="https://www.allelitewrestling.com", + calendar_class=AewCalendar, + multi_calendar=False, + ), ShowsIntegration( id="shows", name="TV Shows", diff --git a/tests/integrations/test_aew.py b/tests/integrations/test_aew.py new file mode 100644 index 0000000..3144543 --- /dev/null +++ b/tests/integrations/test_aew.py @@ -0,0 +1,298 @@ +import pytest +from datetime import datetime +from unittest.mock import patch, MagicMock + +from integrations.aew import parse_aew_datetime, get_aew_events, AewCalendar + + +class TestParseAewDatetime: + """Tests for parse_aew_datetime function.""" + + def test_pacific_time(self): + """Test parsing Pacific Time.""" + result = parse_aew_datetime("FEBRUARY 11, 2026", "4:30pm PT") + assert result is not None + # 4:30 PM PT = 12:30 AM UTC next day (8 hour offset) + assert result.hour == 0 + assert result.minute == 30 + assert result.day == 12 # Next day in UTC + assert result.month == 2 + assert result.year == 2026 + + def test_eastern_time(self): + """Test parsing Eastern Time.""" + result = parse_aew_datetime("MARCH 15, 2026", "8:00pm ET") + assert result is not None + # 8:00 PM ET = 1:00 AM UTC next day (5 hour offset in March - DST) + assert result.hour == 0 # Midnight UTC + assert result.month == 3 + assert result.day == 16 # Next day in UTC + + def test_mountain_time(self): + """Test parsing Mountain Time.""" + result = parse_aew_datetime("MARCH 4, 2026", "5:30pm MT") + assert result is not None + # 5:30 PM MT = 12:30 AM UTC next day (7 hour offset) + assert result.hour == 0 + assert result.minute == 30 + + def test_central_time(self): + """Test parsing Central Time.""" + result = parse_aew_datetime("MARCH 25, 2026", "6:30pm CT") + assert result is not None + # 6:30 PM CT = 11:30 PM UTC same day (5 hour offset in March - DST) + assert result.hour == 23 + assert result.minute == 30 + + def test_australian_time(self): + """Test parsing Australian Eastern Daylight Time.""" + result = parse_aew_datetime("FEBRUARY 14, 2026", "6:30pm AEDT") + assert result is not None + # 6:30 PM AEDT = 7:30 AM UTC same day (AEDT is UTC+11) + assert result.hour == 7 + assert result.minute == 30 + assert result.day == 14 + + def test_tba_defaults_to_8pm_et(self): + """Test that TBA time defaults to 8:00 PM ET.""" + result = parse_aew_datetime("AUGUST 30, 2026", "TBA") + assert result is not None + # 8:00 PM ET in August (EDT) = 12:00 AM UTC next day + assert result.hour == 0 + assert result.day == 31 # Next day + + def test_tba_uppercase(self): + """Test TBA is case-insensitive.""" + result = parse_aew_datetime("JANUARY 15, 2026", "tba") + assert result is not None + + def test_empty_time_defaults_to_8pm_et(self): + """Test that empty time defaults to 8:00 PM ET.""" + result = parse_aew_datetime("JANUARY 15, 2026", "") + assert result is not None + + def test_invalid_date_returns_none(self): + """Test that invalid date returns None.""" + result = parse_aew_datetime("INVALID DATE", "8:00pm ET") + assert result is None + + def test_invalid_time_format_returns_none(self): + """Test that invalid time format returns None.""" + result = parse_aew_datetime("FEBRUARY 11, 2026", "invalid") + assert result is None + + def test_lowercase_date(self): + """Test that date parsing handles lowercase.""" + result = parse_aew_datetime("february 11, 2026", "4:30pm PT") + assert result is not None + assert result.month == 2 + assert result.day == 12 # UTC next day + + def test_all_months(self): + """Test parsing all months.""" + months = [ + ("JANUARY", 1), ("FEBRUARY", 2), ("MARCH", 3), ("APRIL", 4), + ("MAY", 5), ("JUNE", 6), ("JULY", 7), ("AUGUST", 8), + ("SEPTEMBER", 9), ("OCTOBER", 10), ("NOVEMBER", 11), ("DECEMBER", 12) + ] + for month_name, month_num in months: + result = parse_aew_datetime(f"{month_name} 15, 2026", "12:00pm ET") + assert result is not None + # Note: month in result may differ due to UTC conversion + + +class TestGetAewEvents: + """Tests for get_aew_events function with mocked responses.""" + + @patch("integrations.aew.requests.get") + def test_success(self, mock_get): + """Test successful parsing of AEW events page.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + UPCOMING EVENTS + FEBRUARY 11, 2026 + AEW Dynamite: Ontario + City + Ontario, CA + Venue + Toyota Arena + Time + 4:30pm PT + BUY TICKETS + FEBRUARY 14, 2026 + AEW Grand Slam: Australia + City + Sydney, AUS + Venue + Qudos Bank Arena + Time + 6:30pm AEDT + BUY TICKETS + + + """ + mock_get.return_value = mock_response + + events = get_aew_events() + assert len(events) == 2 + assert events[0]["title"] == "AEW Dynamite: Ontario" + assert "Toyota Arena" in events[0]["location"] + assert events[1]["title"] == "AEW Grand Slam: Australia" + + @patch("integrations.aew.requests.get") + def test_api_failure_returns_empty(self, mock_get): + """Test that API failure returns empty list.""" + mock_response = MagicMock() + mock_response.status_code = 500 + mock_get.return_value = mock_response + + events = get_aew_events() + assert events == [] + + @patch("integrations.aew.requests.get") + def test_empty_page_returns_empty(self, mock_get): + """Test that empty page returns empty list.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = "" + mock_get.return_value = mock_response + + events = get_aew_events() + assert events == [] + + @patch("integrations.aew.requests.get") + def test_request_exception_returns_empty(self, mock_get): + """Test that request exception returns empty list.""" + mock_get.side_effect = Exception("Network error") + + events = get_aew_events() + assert events == [] + + @patch("integrations.aew.requests.get") + def test_duplicate_events_deduplicated(self, mock_get): + """Test that duplicate events are deduplicated by UID.""" + mock_response = MagicMock() + mock_response.status_code = 200 + # Same event appearing twice + mock_response.text = """ + FEBRUARY 11, 2026 + AEW Dynamite: Ontario + Ontario, CA + Toyota Arena + 4:30pm PT + FEBRUARY 11, 2026 + AEW Dynamite: Ontario + Ontario, CA + Toyota Arena + 4:30pm PT + """ + mock_get.return_value = mock_response + + events = get_aew_events() + # Should only have one event due to deduplication + assert len(events) == 1 + + +class TestAewCalendar: + """Tests for AewCalendar class.""" + + @patch("integrations.aew.get_aew_events") + def test_fetch_events_success(self, mock_get_events): + """Test successful event fetching.""" + mock_get_events.return_value = [ + { + "uid": "aew-dynamite-feb-11", + "title": "AEW Dynamite: Ontario", + "start": datetime(2026, 2, 12, 0, 30), + "location": "Toyota Arena, Ontario, CA", + "description": "", + } + ] + + cal = AewCalendar(name="AEW", id="aew", icon="", events=[]) + events = cal.fetch_events() + + assert len(events) == 1 + assert events[0].title == "AEW Dynamite: Ontario" + assert events[0].uid == "aew-dynamite-feb-11" + + @patch("integrations.aew.get_aew_events") + def test_ppv_events_get_longer_duration(self, mock_get_events): + """Test that PPV events get 4 hour duration.""" + mock_get_events.return_value = [ + { + "uid": "aew-revolution-2026", + "title": "AEW: Revolution 2026", + "start": datetime(2026, 3, 15, 23, 0), + "location": "Crypto.com Arena, Los Angeles, CA", + "description": "", + } + ] + + cal = AewCalendar(name="AEW", id="aew", icon="", events=[]) + events = cal.fetch_events() + + assert len(events) == 1 + # PPV should be 4 hours + duration = events[0].end - events[0].start + assert duration.total_seconds() == 4 * 3600 # 4 hours + + @patch("integrations.aew.get_aew_events") + def test_regular_show_gets_shorter_duration(self, mock_get_events): + """Test that regular shows get 3 hour duration.""" + mock_get_events.return_value = [ + { + "uid": "aew-dynamite-feb-11", + "title": "AEW Dynamite: Ontario", + "start": datetime(2026, 2, 12, 0, 30), + "location": "Toyota Arena, Ontario, CA", + "description": "", + } + ] + + cal = AewCalendar(name="AEW", id="aew", icon="", events=[]) + events = cal.fetch_events() + + assert len(events) == 1 + # Regular show should be 3 hours + duration = events[0].end - events[0].start + assert duration.total_seconds() == 3 * 3600 # 3 hours + + @patch("integrations.aew.get_aew_events") + def test_events_sorted_by_start_time(self, mock_get_events): + """Test that events are sorted by start time.""" + mock_get_events.return_value = [ + { + "uid": "aew-event-2", + "title": "Event 2", + "start": datetime(2026, 3, 1), + "location": "", + "description": "", + }, + { + "uid": "aew-event-1", + "title": "Event 1", + "start": datetime(2026, 2, 1), + "location": "", + "description": "", + }, + ] + + cal = AewCalendar(name="AEW", id="aew", icon="", events=[]) + events = cal.fetch_events() + + assert events[0].title == "Event 1" + assert events[1].title == "Event 2" + + @patch("integrations.aew.get_aew_events") + def test_empty_events_returns_empty_list(self, mock_get_events): + """Test that empty events returns empty list.""" + mock_get_events.return_value = [] + + cal = AewCalendar(name="AEW", id="aew", icon="", events=[]) + events = cal.fetch_events() + + assert events == [] diff --git a/tests/integrations/test_wwe.py b/tests/integrations/test_wwe.py index d1fdac5..b569a7d 100644 --- a/tests/integrations/test_wwe.py +++ b/tests/integrations/test_wwe.py @@ -1,108 +1,712 @@ +"""Tests for WWE integration.""" import pytest from datetime import datetime from unittest.mock import patch, MagicMock +from zoneinfo import ZoneInfo -from integrations.wwe import parse_wwe_datetime, WweCalendar +from integrations.wwe import ( + WweCalendar, + classify_show, + get_timezone_for_location, + parse_event_datetime, + scrape_wwe_events, +) -class TestParseWweDatetime: - def test_pm_time(self): - result = parse_wwe_datetime("Sat, Jun 15", "7:30 PM") - assert result.hour == 19 +class TestGetTimezoneForLocation: + """Tests for timezone detection from location strings.""" + + def test_eastern_timezone_states(self): + """Test Eastern timezone detection for various states.""" + eastern_locations = [ + ("Cleveland, OH", "America/New_York"), + ("New York, NY", "America/New_York"), + ("Atlanta, GA", "America/New_York"), + ("Miami, FL", "America/New_York"), + ("Boston, MA", "America/New_York"), + ] + for location, expected_tz in eastern_locations: + result = get_timezone_for_location(location) + assert str(result) == expected_tz, f"Failed for {location}" + + def test_central_timezone_states(self): + """Test Central timezone detection.""" + central_locations = [ + ("Dallas, TX", "America/Chicago"), + ("Chicago, IL", "America/Chicago"), + ("Memphis, TN", "America/Chicago"), + ("Minneapolis, MN", "America/Chicago"), + ] + for location, expected_tz in central_locations: + result = get_timezone_for_location(location) + assert str(result) == expected_tz, f"Failed for {location}" + + def test_pacific_timezone_states(self): + """Test Pacific timezone detection.""" + pacific_locations = [ + ("Las Vegas, NV", "America/Los_Angeles"), + ("Los Angeles, CA", "America/Los_Angeles"), + ("Seattle, WA", "America/Los_Angeles"), + ("Portland, OR", "America/Los_Angeles"), + ] + for location, expected_tz in pacific_locations: + result = get_timezone_for_location(location) + assert str(result) == expected_tz, f"Failed for {location}" + + def test_mountain_timezone_states(self): + """Test Mountain timezone detection.""" + result = get_timezone_for_location("Denver, CO") + assert str(result) == "America/Denver" + + def test_arizona_no_dst(self): + """Test Arizona uses non-DST timezone.""" + result = get_timezone_for_location("Phoenix, AZ") + assert str(result) == "America/Phoenix" + + def test_international_uk(self): + """Test UK timezone detection.""" + result = get_timezone_for_location("London, UK") + assert str(result) == "Europe/London" + + def test_international_germany(self): + """Test Germany timezone detection.""" + result = get_timezone_for_location("Berlin, Germany") + assert str(result) == "Europe/Berlin" + + def test_canada_eastern(self): + """Test Canadian Eastern timezone.""" + locations = [ + ("Toronto, ON", "America/Toronto"), + ("Montreal, QC", "America/Toronto"), + ] + for location, expected_tz in locations: + result = get_timezone_for_location(location) + assert str(result) == expected_tz, f"Failed for {location}" + + def test_unknown_defaults_to_eastern(self): + """Test unknown locations default to Eastern time.""" + result = get_timezone_for_location("Unknown City, XX") + assert str(result) == "America/New_York" + + +class TestParseEventDatetime: + """Tests for parse_event_datetime function.""" + + def test_parse_basic_date_time(self): + """Test parsing a standard date/time string.""" + result = parse_event_datetime("Feb 9", "7:30 PM", "Cleveland, OH") + # Should be 7:30 PM Eastern converted to UTC (00:30 next day in winter) + assert result.hour == 0 or result.hour == 23 # Depends on DST assert result.minute == 30 - assert result.month == 6 - assert result.day == 15 + assert result.month == 2 + assert result.day in [9, 10] # Could be next day in UTC + + def test_parse_pm_time(self): + """Test PM time conversion.""" + result = parse_event_datetime("Jun 15", "7:00 PM", "Dallas, TX") + # 7 PM Central = 12 AM or 1 AM UTC next day + assert result.tzinfo == ZoneInfo("UTC") - def test_am_time(self): - result = parse_wwe_datetime("Mon, Jan 6", "10:00 AM") - assert result.hour == 10 + def test_parse_am_time(self): + """Test AM time conversion.""" + result = parse_event_datetime("Jun 15", "10:00 AM", "New York, NY") + # 10 AM Eastern assert result.minute == 0 - def test_noon(self): - result = parse_wwe_datetime("Tue, Mar 4", "12:00 PM") - assert result.hour == 12 + def test_parse_noon(self): + """Test noon (12:00 PM) handling.""" + result = parse_event_datetime("Mar 4", "12:00 PM", "Chicago, IL") + # 12 PM Central = 6 PM UTC + assert result.tzinfo == ZoneInfo("UTC") - def test_midnight(self): - result = parse_wwe_datetime("Wed, Feb 5", "12:00 AM") - assert result.hour == 0 + def test_parse_midnight(self): + """Test midnight (12:00 AM) handling.""" + result = parse_event_datetime("Feb 5", "12:00 AM", "Los Angeles, CA") + # 12 AM Pacific = 8 AM UTC + assert result.tzinfo == ZoneInfo("UTC") + + def test_all_months(self): + """Test all month abbreviations are recognized.""" + months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + for i, month in enumerate(months, 1): + result = parse_event_datetime(f"{month} 1", "8:00 PM", "New York, NY") + assert result.month == i or result.month == (i % 12) + 1 # May roll to next month in UTC - def test_uses_current_year(self): - result = parse_wwe_datetime("Thu, Jul 10", "8:00 PM") - assert result.year == datetime.now().year + def test_ordinal_suffix_removed(self): + """Test ordinal suffixes (st, nd, rd, th) are handled.""" + result = parse_event_datetime("April 18th", "4:00 PM", "Las Vegas, NV") + assert result.day == 18 or result.day == 19 # UTC conversion may shift day - def test_invalid_date_format_raises(self): + def test_invalid_date_raises(self): + """Test invalid date format raises ValueError.""" with pytest.raises(ValueError): - parse_wwe_datetime("Invalid", "8:00 PM") + parse_event_datetime("Invalid", "8:00 PM", "New York, NY") - def test_invalid_time_format_raises(self): + def test_invalid_time_raises(self): + """Test invalid time format raises ValueError.""" with pytest.raises(ValueError): - parse_wwe_datetime("Sat, Jun 15", "invalid") + parse_event_datetime("Jun 15", "invalid", "New York, NY") def test_invalid_month_raises(self): + """Test invalid month raises ValueError.""" with pytest.raises(ValueError): - parse_wwe_datetime("Sat, Xyz 15", "8:00 PM") + parse_event_datetime("Xyz 15", "8:00 PM", "New York, NY") - def test_invalid_time_parts_raises(self): - with pytest.raises(ValueError): - parse_wwe_datetime("Sat, Jun 15", "8 PM") - def test_all_months(self): - months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] - for i, month in enumerate(months, 1): - result = parse_wwe_datetime(f"Mon, {month} 1", "8:00 PM") - assert result.month == i +class TestScrapeWweEvents: + """Tests for scrape_wwe_events function.""" + @patch("integrations.wwe.requests.get") + def test_successful_scrape(self, mock_get): + """Test successful scraping of WWE events page.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Friday Night SmackDown +

+
+ Friday Feb 13 Dallas, TX 6:30 PM | American Airlines Center +
+
+ + + """ + mock_get.return_value = mock_response + + events = scrape_wwe_events() + + assert len(events) == 2 + assert events[0]["title"] == "Monday Night RAW" + assert events[0]["venue"] == "Rocket Arena" + assert events[0]["city_state"] == "Cleveland, OH" + assert events[1]["title"] == "Friday Night SmackDown" -class TestWweCalendarFetchEvents: @patch("integrations.wwe.requests.get") - def test_success(self, mock_get): + def test_skips_duplicate_events(self, mock_get): + """Test that duplicate events are skipped.""" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.json.return_value = [ + # Same event appearing twice + mock_response.text = """ + + + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+ + + """ + mock_get.return_value = mock_response + + events = scrape_wwe_events() + + assert len(events) == 1 + + @patch("integrations.wwe.requests.get") + def test_handles_request_error(self, mock_get): + """Test handling of request errors.""" + mock_get.side_effect = Exception("Network error") + + with pytest.raises(Exception): + scrape_wwe_events() + + @patch("integrations.wwe.requests.get") + def test_skips_malformed_rows(self, mock_get): + """Test that rows without required elements are skipped.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + + + +
No event data here
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+ + + """ + mock_get.return_value = mock_response + + events = scrape_wwe_events() + + assert len(events) == 1 + + +class TestWweCalendar: + """Tests for WweCalendar class.""" + + @patch("integrations.wwe.scrape_wwe_events") + def test_fetch_events_success(self, mock_scrape): + """Test successful event fetching.""" + mock_scrape.return_value = [ { - "type": "event", - "nid": "12345", + "uid": "wwe-test-event-20260215", + "title": "Monday Night RAW", + "start": datetime(2026, 2, 16, 0, 30, tzinfo=ZoneInfo("UTC")), + "end": datetime(2026, 2, 16, 3, 30, tzinfo=ZoneInfo("UTC")), + "venue": "Rocket Arena", + "city_state": "Cleveland, OH", + "link": "https://www.wwe.com/event/raw", + } + ] + + cal = WweCalendar(name="WWE", id="wwe", icon="", events=[]) + events = cal.fetch_events() + + assert len(events) == 1 + assert events[0].title == "Monday Night RAW" + assert events[0].uid == "wwe-test-event-20260215" + assert "Rocket Arena" in events[0].location + assert "Cleveland, OH" in events[0].location + + @patch("integrations.wwe.scrape_wwe_events") + def test_fetch_events_includes_link_in_description(self, mock_scrape): + """Test that event link is included in description.""" + mock_scrape.return_value = [ + { + "uid": "wwe-test-20260215", "title": "WrestleMania", - "date": f"Sat, Jun 15", - "time": "7:00 PM", - "teaser_title": "WrestleMania XL", - "location": "Philadelphia, PA", - "link": "/events/wrestlemania", + "start": datetime(2026, 4, 18, 23, 0, tzinfo=ZoneInfo("UTC")), + "end": datetime(2026, 4, 19, 3, 0, tzinfo=ZoneInfo("UTC")), + "venue": "Allegiant Stadium", + "city_state": "Las Vegas, NV", + "link": "https://www.wwe.com/event/wrestlemania", } ] - mock_get.return_value = mock_response cal = WweCalendar(name="WWE", id="wwe", icon="", events=[]) events = cal.fetch_events() + + assert "https://www.wwe.com/event/wrestlemania" in events[0].description + + @patch("integrations.wwe.scrape_wwe_events") + def test_fetch_events_handles_scrape_failure(self, mock_scrape): + """Test handling of scrape failures.""" + import requests + mock_scrape.side_effect = requests.RequestException("Failed to fetch") + + cal = WweCalendar(name="WWE", id="wwe", icon="", events=[]) + + with pytest.raises(Exception): + cal.fetch_events() + + +class TestEventDuration: + """Tests for event duration based on event type.""" + + @patch("integrations.wwe.requests.get") + def test_ple_duration_4_hours(self, mock_get): + """Test Premium Live Events get 4-hour duration.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + +
+

+ WrestleMania Saturday +

+
+ Saturday Apr 18 Las Vegas, NV 4:00 PM | Allegiant Stadium +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events() + assert len(events) == 1 - assert events[0].title == "WrestleMania" - assert events[0].uid == "wwe-12345" + duration = events[0]["end"] - events[0]["start"] + assert duration.total_seconds() == 4 * 3600 # 4 hours @patch("integrations.wwe.requests.get") - def test_non_event_items_skipped(self, mock_get): + def test_nxt_duration_2_hours(self, mock_get): + """Test NXT events get 2-hour duration.""" mock_response = MagicMock() mock_response.status_code = 200 - mock_response.json.return_value = [ - {"type": "ad", "nid": "1", "title": "Ad"}, - { - "type": "event", "nid": "2", "title": "Raw", - "date": "Mon, Jan 6", "time": "8:00 PM", - "location": "NYC", - }, - ] + mock_response.text = """ + + + + +
+

+ NXT +

+
+ Tuesday Feb 24 Atlanta, GA 7:30 PM | Center Stage +
+
+ """ mock_get.return_value = mock_response - cal = WweCalendar(name="WWE", id="wwe", icon="", events=[]) - events = cal.fetch_events() + events = scrape_wwe_events() + assert len(events) == 1 + duration = events[0]["end"] - events[0]["start"] + assert duration.total_seconds() == 2 * 3600 # 2 hours @patch("integrations.wwe.requests.get") - def test_api_failure(self, mock_get): + def test_regular_show_duration_3_hours(self, mock_get): + """Test regular shows (RAW, SmackDown) get 3-hour duration.""" mock_response = MagicMock() - mock_response.status_code = 500 + mock_response.status_code = 200 + mock_response.text = """ + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+ """ mock_get.return_value = mock_response - cal = WweCalendar(name="WWE", id="wwe", icon="", events=[]) - with pytest.raises(Exception): - cal.fetch_events() + events = scrape_wwe_events() + + assert len(events) == 1 + duration = events[0]["end"] - events[0]["start"] + assert duration.total_seconds() == 3 * 3600 # 3 hours + + +class TestClassifyShow: + """Tests for show type classification.""" + + def test_classify_raw(self): + """Test RAW show classification.""" + assert classify_show("Monday Night RAW") == "raw" + assert classify_show("RAW") == "raw" + assert classify_show("monday night raw") == "raw" + + def test_classify_smackdown(self): + """Test SmackDown show classification.""" + assert classify_show("Friday Night SmackDown") == "smackdown" + assert classify_show("SmackDown") == "smackdown" + assert classify_show("SMACKDOWN") == "smackdown" + + def test_classify_nxt(self): + """Test NXT show classification.""" + assert classify_show("NXT") == "nxt" + assert classify_show("NXT Deadline") == "nxt" + assert classify_show("NXT Battleground") == "nxt" + + def test_classify_ple(self): + """Test Premium Live Event classification.""" + assert classify_show("WrestleMania Saturday") == "ple" + assert classify_show("WrestleMania 41") == "ple" + assert classify_show("SummerSlam 2026") == "ple" + assert classify_show("Royal Rumble") == "ple" + assert classify_show("Elimination Chamber") == "ple" + assert classify_show("Survivor Series") == "ple" + assert classify_show("Money in the Bank") == "ple" + assert classify_show("Backlash") == "ple" + assert classify_show("Clash at the Castle") == "ple" + assert classify_show("Bad Blood") == "ple" + assert classify_show("Saturday Night's Main Event") == "ple" + + def test_classify_other(self): + """Test house shows and other events.""" + assert classify_show("WWE Live") == "other" + assert classify_show("WWE Supershow") == "other" + assert classify_show("Holiday Tour") == "other" + + +class TestShowFiltering: + """Tests for show filtering in scrape_wwe_events.""" + + @patch("integrations.wwe.requests.get") + def test_filter_raw_only(self, mock_get): + """Test filtering for RAW events only.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Friday Night SmackDown +

+
+ Friday Feb 13 Dallas, TX 6:30 PM | American Airlines Center +
+
+

+ NXT +

+
+ Tuesday Feb 24 Atlanta, GA 7:30 PM | Center Stage +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events(show="raw") + + assert len(events) == 1 + assert events[0]["title"] == "Monday Night RAW" + assert events[0]["show_type"] == "raw" + + @patch("integrations.wwe.requests.get") + def test_filter_smackdown_only(self, mock_get): + """Test filtering for SmackDown events only.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Friday Night SmackDown +

+
+ Friday Feb 13 Dallas, TX 6:30 PM | American Airlines Center +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events(show="smackdown") + + assert len(events) == 1 + assert events[0]["title"] == "Friday Night SmackDown" + assert events[0]["show_type"] == "smackdown" + + @patch("integrations.wwe.requests.get") + def test_filter_ple_only(self, mock_get): + """Test filtering for Premium Live Events only.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + +
+

+ WrestleMania Saturday +

+
+ Saturday Apr 18 Las Vegas, NV 4:00 PM | Allegiant Stadium +
+
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events(show="ple") + + assert len(events) == 1 + assert events[0]["title"] == "WrestleMania Saturday" + assert events[0]["show_type"] == "ple" + + @patch("integrations.wwe.requests.get") + def test_filter_all_returns_everything(self, mock_get): + """Test that 'all' filter returns all events.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Friday Night SmackDown +

+
+ Friday Feb 13 Dallas, TX 6:30 PM | American Airlines Center +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events(show="all") + + assert len(events) == 2 + + @patch("integrations.wwe.requests.get") + def test_filter_none_returns_everything(self, mock_get): + """Test that None filter returns all events.""" + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.text = """ + + + + + + + +
+

+ Monday Night RAW +

+
+ Monday Feb 9 Cleveland, OH 7:30 PM | Rocket Arena +
+
+

+ Friday Night SmackDown +

+
+ Friday Feb 13 Dallas, TX 6:30 PM | American Airlines Center +
+
+ """ + mock_get.return_value = mock_response + + events = scrape_wwe_events(show=None) + + assert len(events) == 2 + + +class TestWweCalendarWithShowFilter: + """Tests for WweCalendar with show filtering.""" + + @patch("integrations.wwe.scrape_wwe_events") + def test_fetch_events_with_raw_filter(self, mock_scrape): + """Test fetching RAW events only.""" + mock_scrape.return_value = [ + { + "uid": "wwe-monday-night-raw-20260209", + "title": "Monday Night RAW", + "start": datetime(2026, 2, 10, 0, 30, tzinfo=ZoneInfo("UTC")), + "end": datetime(2026, 2, 10, 3, 30, tzinfo=ZoneInfo("UTC")), + "venue": "Rocket Arena", + "city_state": "Cleveland, OH", + "link": "https://www.wwe.com/event/raw", + "show_type": "raw", + } + ] + + cal = WweCalendar(name="WWE RAW", id="wwe-raw", icon="", events=[]) + events = cal.fetch_events(show="raw") + + mock_scrape.assert_called_once_with(show="raw") + assert len(events) == 1 + assert events[0].title == "Monday Night RAW" + + @patch("integrations.wwe.scrape_wwe_events") + def test_fetch_events_with_ple_filter(self, mock_scrape): + """Test fetching PLE events only.""" + mock_scrape.return_value = [ + { + "uid": "wwe-wrestlemania-saturday-20260418", + "title": "WrestleMania Saturday", + "start": datetime(2026, 4, 18, 23, 0, tzinfo=ZoneInfo("UTC")), + "end": datetime(2026, 4, 19, 3, 0, tzinfo=ZoneInfo("UTC")), + "venue": "Allegiant Stadium", + "city_state": "Las Vegas, NV", + "link": "https://www.wwe.com/event/wrestlemania", + "show_type": "ple", + } + ] + + cal = WweCalendar(name="WWE PLEs", id="wwe-ple", icon="", events=[]) + events = cal.fetch_events(show="ple") + + mock_scrape.assert_called_once_with(show="ple") + assert len(events) == 1 + assert events[0].title == "WrestleMania Saturday"