"""Models related to the leaderboard section in Tibia.com."""
import datetime
import re
from typing import List, Optional
from tibiapy import abc, errors
from tibiapy.utils import get_tibia_url, parse_form_data, parse_pagination, parse_tibia_datetime, parse_tibiacom_content
__all__ = (
'Leaderboard',
'LeaderboardEntry',
'LeaderboardRotation',
)
rotation_end_pattern = re.compile(r"ends on ([^)]+)")
[docs]class Leaderboard(abc.Serializable):
"""Represents the Tibiadrome leaderboards.
.. versionadded:: 5.0.0
Attributes
----------
world: :class:`str`
The world this leaderboards are for.
available_worlds: :class:`list` of :class:`str`
The worlds available for selection.
rotation: :class:`LeaderboardRotation`
The rotation this leaderboards' entries are for.
available_rotations: :class:`list` of :class:`LeaderboardRotation`
The available rotations for selection.
entries: list of :class:`LeaderboardEntry`
The list of entries in this leaderboard.
last_update: :class:`datetime.timedelta`
How long ago was the currently displayed data updated. Only available for the current rotation.
page: :class:`int`
The page number being displayed.
total_pages: :class:`int`
The total number of pages.
results_count: :class:`int`
The total amount of entries in this rotation. These may be shown in another page.
"""
__slots__ = (
"world",
"rotation",
"last_update",
"page",
"total_pages",
"results_count",
"entries",
"available_worlds",
"available_rotations",
)
def __init__(self, world, rotation, **kwargs):
self.world: str = world
self.rotation: LeaderboardRotation = rotation
self.available_worlds: List[str] = kwargs.get("available_worlds", [])
self.available_rotations: List[LeaderboardRotation] = kwargs.get("available_rotations", [])
self.entries: List[LeaderboardEntry] = kwargs.get("entries", [])
self.last_update: Optional[datetime.timedelta] = kwargs.get("last_update")
self.page: int = kwargs.get("page", 1)
self.total_pages: int = kwargs.get("total_pages", 1)
self.results_count: int = kwargs.get("results_count", 0)
def __repr__(self):
return f"<{self.__class__.__name__} world={self.world!r} rotation={self.rotation!r} " \
f"page={self.page!r} total_pages={self.total_pages!r} results_count={self.results_count!r}>"
@property
def url(self):
""":class:`str`: The URL to the current leaderboard."""
return self.get_url(self.world, self.rotation.rotation_id, self.page)
@property
def previous_page_url(self):
""":class:`str`: The URL to the previous page of the current leaderboard results, if there's any."""
return self.get_page_url(self.page - 1) if self.page > 1 else None
@property
def next_page_url(self):
""":class:`str`: The URL to the next page of the current leaderboard results, if there's any."""
return self.get_page_url(self.page + 1) if self.page < self.total_pages else None
[docs] def get_page_url(self, page):
"""Get the URL of the leaderboard at a specific page, with the current date parameters.
Parameters
----------
page: :class:`int`
The desired page.
Returns
-------
:class:`str`
The URL to the desired page.
Raises
------
ValueError
If the specified page is zer or less.
"""
return self.get_url(self.world, self.rotation.rotation_id, page)
[docs] @classmethod
def get_url(cls, world, rotation_id=None, page=1):
"""Get the URL to the leaderboards of a world.
Parameters
----------
world: :class:`str`
The desired world.
rotation_id: :class:`int`
The ID of the desired rotation. If undefined, the current rotation is shown.
page: :class:`int`
The desired page. By default, the first page is returned.
Returns
-------
:class:`str`
The URL to the leaderboard with the desired parameters.
Raises
------
ValueError
If the specified page is zer or less.
"""
if page <= 0:
raise ValueError("page must be 1 or greater")
return get_tibia_url("community", "leaderboards", world=world, rotation=rotation_id, currentpage=page)
[docs] @classmethod
def from_content(cls, content):
"""Parse the content of the leaderboards page.
Parameters
----------
content: :class:`str`
The HTML content of the leaderboards page.
Returns
-------
:class:`Leaderboard`
The ledaerboard if found.
"""
try:
parsed_content = parse_tibiacom_content(content)
tables = parsed_content.find_all("table", {"class": "TableContent"})
form = parsed_content.find("form")
data = parse_form_data(form, include_options=True)
current_world = data["world"]
current_rotation = None
rotations = []
for label, value in data["__options__"]["rotation"].items():
current = False
if "Current" in label:
label = "".join(rotation_end_pattern.findall(label))
current = True
rotation_end = parse_tibia_datetime(label)
rotation = LeaderboardRotation(int(value), rotation_end, current)
if value == data["rotation"]:
current_rotation = rotation
rotations.append(rotation)
leaderboard = cls(current_world, current_rotation)
leaderboard.available_worlds = [w for w in data["__options__"]["world"].values() if w]
leaderboard.available_rotations = rotations
if leaderboard.rotation and leaderboard.rotation.current:
last_update_table = tables[2]
numbers = re.findall(r'(\d+)', last_update_table.text)
if numbers:
leaderboard.last_update = datetime.timedelta(minutes=int(numbers[0]))
leaderboard._parse_entries(tables[-1])
pagination_block = parsed_content.find("small")
pages, total, count = parse_pagination(pagination_block) if pagination_block else (0, 0, 0)
leaderboard.page = pages
leaderboard.total_pages = total
leaderboard.results_count = count
return leaderboard
except (AttributeError, ValueError, KeyError) as e:
raise errors.InvalidContent("content does not belong to the leaderboards", e)
def _parse_entries(self, entries_table):
entries_rows = entries_table.find_all("tr", {'style': True})
for row in entries_rows:
columns_raw = row.find_all("td")
cols = [c.text for c in columns_raw]
rank, name, points = cols
entry = LeaderboardEntry(int(rank.replace(".", "")), name, int(points))
self.entries.append(entry)
[docs]class LeaderboardEntry(abc.BaseCharacter, abc.Serializable):
"""Represents a single leadeboard entry.
Parameters
----------
rank: :class:`int`
The rank of this entry.
name: :class:`str`
The name of the character.
drome_level: :class:`int`
The Tibia Drome level of this entry.
"""
__slots__ = (
"rank",
"name",
"drome_level",
)
def __init__(self, rank, name, drome_level):
self.rank: int = rank
self.name: str = name
self.drome_level: int = drome_level
def __repr__(self):
return f"<{self.__class__.__name__} rank={self.rank} name={self.name!r} drome_level={self.drome_level}>"
[docs]class LeaderboardRotation(abc.Serializable):
"""A leaderboard rotation.
.. versionadded:: 5.0.0
Parameters
----------
rotation_id: :class:`int`
The internal ID of the rotation.
current: :class:`bool`
Whether this is the currently running rotation or not.
end_date: :class:`datetime.datetime`
The date and time when this rotation ends.
"""
__slots__ = (
"rotation_id",
"current",
"end_date",
)
def __init__(self, rotation_id, end_date, current=False):
self.rotation_id: int = rotation_id
self.end_date: datetime.datetime = end_date
self.current: bool = current
def __repr__(self):
return f"<{self.__class__.__name__} rotation_id={self.rotation_id} end_date={self.end_date!r} " \
f"current={self.current!r}>"
def __eq__(self, other):
if isinstance(other, self.__class__):
return other.rotation_id == self.rotation_id
return False