"""Parsers for the news archive and news articles."""
import datetime
import re
import urllib.parse
from typing import Optional, Union
import bs4
from tibiapy.builders.news import NewsArchiveBuilder, NewsBuilder
from tibiapy.enums import NewsCategory, NewsType
from tibiapy.errors import InvalidContentError
from tibiapy.models.news import News, NewsArchive, NewsEntry
from tibiapy.utils import (
clean_text,
parse_form_data,
parse_link_info,
parse_tibia_date,
parse_tibiacom_content,
parse_tibiacom_tables,
try_enum,
)
__all__ = (
"NewsParser",
"NewsArchiveParser",
)
ICON_PATTERN = re.compile(r"newsicon_([^_]+)_(?:small|big)")
[docs]
class NewsArchiveParser:
"""Parse Tibia.com content from the news archive section."""
[docs]
@classmethod
def from_content(cls, content: str) -> NewsArchive:
"""Get a list of news from the HTML content of the news search page.
Parameters
----------
content:
The HTML content of the page.
Returns
-------
The news archive with the news found.
Raises
------
InvalidContent
If content is not the HTML of a news search's page.
"""
parsed_content = parse_tibiacom_content(content)
tables = parse_tibiacom_tables(parsed_content)
if "News Archive Search" not in tables:
raise InvalidContentError("content is not from the news archive section in Tibia.com")
form = parsed_content.select_one("form")
builder = NewsArchiveBuilder()
cls._parse_filter_table(builder, form)
if "Search Results" in tables:
rows = tables["Search Results"].select("tr.Odd, tr.Even")
for row in rows:
cols_raw = row.select("td")
if len(cols_raw) != 3:
continue
builder.add_entry(cls._parse_entry(cols_raw))
return builder.build()
@classmethod
def _parse_filter_table(cls, builder: NewsArchiveBuilder, form: bs4.Tag) -> None:
form_data = parse_form_data(form)
builder.from_date(datetime.date(
int(form_data.values["filter_begin_year"]),
int(form_data.values["filter_begin_month"]),
int(form_data.values["filter_begin_day"]),
)).to_date(
datetime.date(
int(form_data.values["filter_end_year"]),
int(form_data.values["filter_end_month"]),
int(form_data.values["filter_end_day"]),
),
)
for news_type in NewsType: # type: NewsType
if news_type.filter_name in form_data.values_multiple:
builder.add_type(news_type)
for category in NewsCategory: # type: NewsCategory
if category.filter_name in form_data.values_multiple:
builder.add_category(category)
@classmethod
def _parse_entry(cls, cols_raw: list[bs4.Tag]) -> NewsEntry:
img = cols_raw[0].select_one("img")
img_url = img["src"]
category_name = ICON_PATTERN.search(img_url)
category = try_enum(NewsCategory, category_name.group(1))
for br in cols_raw[1].select("br"):
br.replace_with("\n")
date_str, news_type_str = cols_raw[1].text.splitlines()
date = parse_tibia_date(date_str)
news_type_str = clean_text(news_type_str)
news_type = try_enum(NewsType, news_type_str)
title = cols_raw[2].text
news_link = parse_link_info(cols_raw[2].select_one("a"))
news_id = int(news_link["query"]["id"])
return NewsEntry(id=news_id, title=title, type=news_type, category=category, published_on=date)
[docs]
class NewsParser:
"""Parser for news articles from Tibia.com."""
[docs]
@classmethod
def from_content(cls, content: str, news_id: int = 0) -> Optional[News]:
"""Get a news entry by its HTML content from Tibia.com.
Notes
-----
Since there's no way to obtain the entry's ID from the page contents, it will always be 0.
A news_id can be passed to set the news_id of the resulting object.
Parameters
----------
content:
The HTML content of the page.
news_id:
The news_id belonging to the content being parsed.
Returns
-------
The news article found in the page.
Raises
------
InvalidContent
If content is not the HTML of a news' page.
"""
if "News not found" in content:
return None
try:
parsed_content = parse_tibiacom_content(content)
builder = NewsBuilder().id(news_id)
# Read Information from the headline
headline = parsed_content.select_one("div.NewsHeadline")
img = headline.select_one("img")
img_url = img["src"]
category_name = ICON_PATTERN.search(img_url)
builder.category(try_enum(NewsCategory, category_name.group(1)))
title_div = headline.select_one("div.NewsHeadlineText")
builder.title(clean_text(title_div))
date_div = headline.select_one("div.NewsHeadlineDate")
date_str = clean_text(date_div).replace("-", "").strip()
builder.published_on(parse_tibia_date(date_str))
# Read the page's content.
content_table = parsed_content.select_one("table")
content_row = content_table.select_one("td")
builder.content(content_row.encode_contents().decode())
if thread_link := content_table.select_one("div.NewsForumLink a"):
url = urllib.parse.urlparse(thread_link["href"])
query = urllib.parse.parse_qs(url.query)
builder.thread_id(int(query["threadid"][0]))
return builder.build()
except AttributeError as e:
raise InvalidContentError("content is not from the news archive section in Tibia.com") from e