Source code for tibiapy.event

"""Models related to the event schedule section in Tibia.com."""

import re
import time
import datetime

from typing import List, Optional

from tibiapy import abc
from tibiapy.utils import get_tibia_url, parse_popup, parse_tibiacom_content

__all__ = (
    'EventSchedule',
    'EventEntry',
)

month_year_regex = re.compile(r'([A-z]+)\s(\d+)')


[docs]class EventSchedule(abc.Serializable): """Represents the event's calendar in Tibia.com. Attributes ---------- month: :class:`int` The month being displayed. Note that some days from the previous and next month may be included too. year: :class:`int` The year being displayed. events: :class:`list` of :class:`EventEntry` A list of events that happen during this month. It might include some events from the previous and next months as well. """ __slots__ = ( 'month', 'year', 'events', ) def __init__(self, month, year, **kwargs): self.month: int = month self.year: int = year self.events: List[EventEntry] = kwargs.get("events", []) def __repr__(self): return f"<{self.__class__.__name__} month={self.month} year={self.year}>" @property def url(self): """:class:`str`: Get the URL to the event calendar with the current parameters.""" return self.get_url(self.month, self.year)
[docs] def get_events_on(self, date): """Get a list of events that are active during the specified desired_date. Parameters ---------- date: :class:`datetime.date` The date to check. Returns ------- :class:`list` of :class:`EventEntry` The events that are active during the desired_date, if any. Notes ----- Dates outside the calendar's month and year may yield unexpected results. """ def is_between(start, end, desired_date): start = start or datetime.date.min end = end or datetime.date.max return start <= desired_date <= end return [e for e in self.events if is_between(e.start_date, e.end_date, date)]
[docs] @classmethod def get_url(cls, month=None, year=None): """Get the URL to the Event Schedule or Event Calendar on Tibia.com. Notes ----- If no parameters are passed, it will show the calendar for the current month and year. Tibia.com limits the dates that the calendar displays, passing a month and year far from the current ones may result in the response being for the current month and year instead. Parameters ---------- month: :class:`int`, optional The desired month. year: :class:`int`, optional The desired year. Returns ------- :class:`str` The URL to the calendar with the given parameters. """ return get_tibia_url("news", "eventcalendar", calendarmonth=month, calendaryear=year)
[docs] @classmethod def from_content(cls, content): """Create an instance of the class from the html content of the event's calendar. Parameters ---------- content: :class:`str` The HTML content of the page. Returns ------- :class:`EventSchedule` The event calendar contained in the page Raises ------ InvalidContent If content is not the HTML of the event's schedule page. """ parsed_content = parse_tibiacom_content(content) month_year_div = parsed_content.find("div", {"class": "eventscheduleheaderdateblock"}) month, year = month_year_regex.search(month_year_div.text).groups() month = time.strptime(month, "%B").tm_mon year = int(year) schedule = cls(month, year) events_table = parsed_content.find("table", {"id": "eventscheduletable"}) day_cells = events_table.find_all("td") # Keep track of events that are ongoing ongoing_events = [] # Keep track of all events present in that day ongoing_day = 1 first_day = True for day_cell in day_cells: day_div = day_cell.find("div") day = int(day_div.text) # The first cells may belong to the previous month if ongoing_day < day: month -= 1 # The last cells may belong to the last month if day < ongoing_day: month += 1 if month > 12: month = 1 year += 1 if month < 1: month = 12 year -= 1 ongoing_day = day + 1 today_events = [] popup_spans = day_cell.find_all('span', attrs={"class": "HelperDivIndicator"}) for popup in popup_spans: title, popup_content = parse_popup(popup["onmouseover"]) divs = popup_content.find_all("div") # Multiple events can be described in the same popup, they come in pairs, title and content. for title, content in zip(*[iter(d.text for d in divs)] * 2): title = title.replace(":", "") content = content.replace("• ", "") event = EventEntry(title, content) today_events.append(event) # If this is not an event that was already ongoing from previous days, add to list if event not in ongoing_events: # Only add a start date if this is not the first day of the calendar # We do not know the actual start date of the event. if not first_day: event.start_date = datetime.date(day=day, month=month, year=year) ongoing_events.append(event) # Check which of the ongoing events did not show up today, meaning it has ended now for pending_event in ongoing_events[:]: if pending_event not in today_events: # If it didn't show up today, it means it ended yesterday. end_date = datetime.date(day=day, month=month, year=year) - datetime.timedelta(days=1) pending_event.end_date = end_date schedule.events.append(pending_event) # Remove from ongoing ongoing_events.remove(pending_event) first_day = False # Add any leftover ongoing events without a end date, as we don't know when they end. schedule.events.extend(ongoing_events) return schedule
[docs]class EventEntry(abc.Serializable): """Represents an event's entry in the calendar. Attributes ---------- title: :class:`str` The title of the event. description: :class:`str` The description of the event. start_date: :class:`datetime.date` The day the event starts. If the event is continuing from the previous month, this will be :obj:`None`. end_date: :class:`datetime.date` The day the event ends. If the event is continuing on the next month, this will be :obj:`None`. """ __slots__ = ( "title", "description", "start_date", "end_date", ) _serializable_properties = ( "duration", ) def __init__(self, title, description, **kwargs): self.title: str = title self.description: str = description self.start_date: Optional[datetime.date] = kwargs.get("start_date") self.end_date: Optional[datetime.date] = kwargs.get("end_date") def __eq__(self, other): return self.title == other.title def __repr__(self): return f"<{self.__class__.__name__} title={self.title!r} description={self.description!r}>" @property def duration(self): """:class:`int`: The number of days this event will be active for.""" return (self.end_date - self.start_date + datetime.timedelta(days=1)).days \ if (self.end_date and self.start_date) else None