"""Event info functionality."""
from __future__ import annotations
import datetime
import re
from bs4 import BeautifulSoup
from .models import Info
from ..config import get_config
from ..countries import COUNTRY_MAP
from ..fetcher import fetch_html
from ..exceptions import NetworkError
from ..utils import extract_text, normalize_whitespace
_config = get_config()
# Month abbreviation to month number mapping
_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,
}
def _parse_date_range(date_text: str | None) -> tuple[datetime.date | None, datetime.date | None]:
"""Parse a date range string into start and end dates.
Parses format like "Dec 17 - 22, 2025" into (2025-12-17, 2025-12-22).
Args:
date_text: The date range string to parse.
Returns:
A tuple of (start_date, end_date), or (None, None) if parsing fails.
"""
if not date_text:
return None, None
# Pattern: "Month Day - EndDay, Year" (e.g., "Dec 17 - 22, 2025")
pattern = r"^([A-Za-z]{3})\s+(\d{1,2})\s*-\s*(\d{1,2}),\s*(\d{4})$"
match = re.match(pattern, date_text.strip())
if not match:
return None, None
month_abbr, start_day_str, end_day_str, year_str = match.groups()
month_num = _MONTH_MAP.get(month_abbr.lower())
if month_num is None:
return None, None
try:
year = int(year_str)
start_day = int(start_day_str)
end_day = int(end_day_str)
start_date = datetime.date(year, month_num, start_day)
end_date = datetime.date(year, month_num, end_day)
return start_date, end_date
except (ValueError, TypeError):
return None, None
def _normalize_regions(tags: list[str]) -> list[str]:
"""Normalize region tags according to business rules.
Rules:
- Allowed main regions: EMEA, Pacific, China, Americas
- If multiple of these main regions are present, return ["international"]
- If exactly one main region is present, keep it as first; then include only valid countries
(any value present in COUNTRY_MAP values). Discard anything else.
- If no main region is present, return only valid countries (if any). Otherwise, return [].
"""
if not tags:
return []
# Deduplicate while preserving order
seen: set[str] = set()
unique_tags: list[str] = []
for t in tags:
t_norm = (t or "").strip()
if not t_norm:
continue
if t_norm not in seen:
seen.add(t_norm)
unique_tags.append(t_norm)
# Canonical main region names and case-insensitive detection
REGION_CANON = {
"emea": "EMEA",
"pacific": "Pacific",
"china": "China",
"americas": "Americas",
}
country_name_set_lower = {v.lower(): v for v in COUNTRY_MAP.values()}
# Resolve main regions case-insensitively to canonical casing
main_regions_canonical: list[str] = []
for t in unique_tags:
key = t.lower()
if key in REGION_CANON and REGION_CANON[key] not in main_regions_canonical:
main_regions_canonical.append(REGION_CANON[key])
# Resolve countries case-insensitively to canonical names
countries_canonical: list[str] = []
for t in unique_tags:
v = country_name_set_lower.get(t.lower())
if v and v not in countries_canonical:
countries_canonical.append(v)
# Exclude any country entries that are actually main regions (e.g., "China")
countries_canonical = [c for c in countries_canonical if c not in REGION_CANON.values()]
if len(main_regions_canonical) >= 2:
# International followed by all detected main regions and valid countries, no duplicates
combined = ["International"] + main_regions_canonical + countries_canonical
seen_out: set[str] = set()
out: list[str] = []
for x in combined:
if x not in seen_out:
seen_out.add(x)
out.append(x)
return out
if len(main_regions_canonical) == 1:
combined = [main_regions_canonical[0]] + countries_canonical
seen_out: set[str] = set()
out: list[str] = []
for x in combined:
if x not in seen_out:
seen_out.add(x)
out.append(x)
return out
# No main regions; include only valid countries
# No main regions; include only valid countries (deduped already)
return countries_canonical
[docs]
def info(event_id: int, timeout: float | None = None) -> Info | None:
"""
Get event header/info.
Args:
event_id: Event ID
timeout: Request timeout in seconds
Returns:
Event info or None if not found
Example:
>>> import vlrdevapi as vlr
>>> event_info = vlr.events.info(event_id=123)
>>> print(f"{event_info.name} - {event_info.prize}")
"""
url = f"{_config.vlr_base}/event/{event_id}"
effective_timeout = timeout if timeout is not None else _config.default_timeout
try:
html = fetch_html(url, effective_timeout)
except NetworkError:
return None
soup = BeautifulSoup(html, "lxml")
header = soup.select_one(".event-header .event-desc-inner")
if not header:
return None
name_el = header.select_one(".wf-title")
subtitle_el = header.select_one(".event-desc-subtitle")
regions: list[str] = []
for a in header.select(".event-tag-container a"):
txt = extract_text(a)
if txt and txt not in regions:
regions.append(txt)
# Extract desc values
def extract_desc_value(label: str) -> str | None:
for item in header.select(".event-desc-item"):
label_el = item.select_one(".event-desc-item-label")
if not label_el or extract_text(label_el) != label:
continue
value_el = item.select_one(".event-desc-item-value")
if value_el:
text = value_el.get_text(" ", strip=True)
if text:
return text
return None
date_text = extract_desc_value("Dates")
prize_text = extract_desc_value("Prize")
if prize_text:
prize_text = normalize_whitespace(prize_text)
location_text = extract_desc_value("Location")
# Parse date range
start_date, end_date = _parse_date_range(date_text)
return Info(
id=event_id,
name=extract_text(name_el),
subtitle=extract_text(subtitle_el) or None,
date_text=date_text,
start_date=start_date,
end_date=end_date,
prize=prize_text,
location=location_text,
regions=_normalize_regions(regions),
)