Source code for vlrdevapi.series.info

"""Series information functionality."""

from __future__ import annotations

import datetime
from bs4 import BeautifulSoup

from .models import Info, TeamInfo
from ._parser import _fetch_team_meta_batch, _parse_note_for_picks_bans, _WHITESPACE_RE, _TIME_RE
from ..config import get_config
from ..fetcher import fetch_html
from ..exceptions import NetworkError
from ..utils import extract_text, extract_id_from_url

_config = get_config()


[docs] def info(match_id: int, timeout: float | None = None) -> Info | None: """ Get series information. Args: match_id: Match ID timeout: Request timeout in seconds Returns: Series information or None if not found Example: >>> import vlrdevapi as vlr >>> info = vlr.series.info(match_id=12345) >>> print(f"{info.teams[0].name} vs {info.teams[1].name}") >>> print(f"Score: {info.score[0]}-{info.score[1]}") """ url = f"{_config.vlr_base}/{match_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(".wf-card.match-header") if not header: return None # Event name and phase event_name = extract_text(header.select_one(".match-header-event div[style*='font-weight']")) or \ extract_text(header.select_one(".match-header-event .wf-title-med")) event_phase = _WHITESPACE_RE.sub(" ", extract_text(header.select_one(".match-header-event-series"))).strip() # Date, time, and patch information date_el = header.select_one(".match-header-date .moment-tz-convert") match_date: datetime.date | None = None time_value: datetime.time | None = None patch_text: str | None = None if date_el and date_el.has_attr("data-utc-ts"): try: dt_attr = date_el.get("data-utc-ts") dt_str = dt_attr if isinstance(dt_attr, str) else None if dt_str: dt = datetime.datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") match_date = dt.date() except Exception: pass time_els = header.select(".match-header-date .moment-tz-convert") if len(time_els) >= 2: time_node = time_els[1] dt_attr = time_node.get("data-utc-ts") dt_str = dt_attr if isinstance(dt_attr, str) else None if dt_str: try: dt_parsed = datetime.datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S") tz_utc = datetime.timezone.utc time_value = datetime.time(hour=dt_parsed.hour, minute=dt_parsed.minute, tzinfo=tz_utc) except Exception: pass if time_value is None: raw = extract_text(time_node) # Handles formats like "2:00 PM PST" and "2:00 PM +02" m = _TIME_RE.match(raw) if m: hour = int(m.group(1)) % 12 minute = int(m.group(2)) if m.group(3).upper() == "PM": hour += 12 tzinfo = None suffix = m.group(4) if suffix and suffix.startswith(("+", "-")) and len(suffix) == 3: sign = 1 if suffix[0] == "+" else -1 offset_hours = int(suffix[1:]) tzinfo = datetime.timezone(sign * datetime.timedelta(hours=offset_hours)) else: tzinfo = datetime.timezone.utc if dt_attr else None time_value = datetime.time(hour=hour, minute=minute, tzinfo=tzinfo) patch_el = header.select_one(".match-header-date div[style*='font-style: italic']") if patch_el: patch_text = extract_text(patch_el) or None # Teams and scores t1_link = header.select_one(".match-header-link.mod-1") t2_link = header.select_one(".match-header-link.mod-2") # Extract team names - get only direct text content, not nested divs # The .wf-title-med may contain nested divs like "(Team Name)" that we want to exclude t1_title_el = header.select_one(".match-header-link.mod-1 .wf-title-med") t2_title_el = header.select_one(".match-header-link.mod-2 .wf-title-med") def extract_direct_text(element) -> str: """Extract only direct text content from element, excluding nested elements.""" if not element: return "" # Get only direct text nodes (not text from nested elements) direct_text = "".join( str(child) for child in element.children if isinstance(child, str) ).strip() # Fallback to full text if no direct text found return direct_text if direct_text else extract_text(element) t1 = extract_direct_text(t1_title_el) t2 = extract_direct_text(t2_title_el) t1_href = t1_link.get("href") if t1_link else None t2_href = t2_link.get("href") if t2_link else None t1_href = t1_href if isinstance(t1_href, str) else None t2_href = t2_href if isinstance(t2_href, str) else None t1_id = extract_id_from_url(t1_href, "team") t2_id = extract_id_from_url(t2_href, "team") t1_short, t1_country, t1_country_code = None, None, None t2_short, t2_country, t2_country_code = None, None, None # Batch fetch team metadata for both teams concurrently team_ids_to_fetch = [tid for tid in [t1_id, t2_id] if tid is not None] if team_ids_to_fetch: team_meta_map = _fetch_team_meta_batch(team_ids_to_fetch, timeout) if t1_id: t1_short, t1_country, t1_country_code = team_meta_map.get(t1_id, (None, None, None)) if t2_id: t2_short, t2_country, t2_country_code = team_meta_map.get(t2_id, (None, None, None)) s1 = header.select_one(".match-header-vs-score-winner") s2 = header.select_one(".match-header-vs-score-loser") raw_score: tuple[int | None, int | None] = (None, None) try: if s1 and s2: raw_score = (int(extract_text(s1)), int(extract_text(s2))) except ValueError: pass notes = header.select(".match-header-vs-note") status_note = extract_text(notes[0]) if notes else "" best_of = extract_text(notes[1]) if len(notes) > 1 else None # Picks/bans team1_info = TeamInfo( id=t1_id, name=t1, short=t1_short, country=t1_country, country_code=t1_country_code, score=raw_score[0], ) team2_info = TeamInfo( id=t2_id, name=t2, short=t2_short, country=t2_country, country_code=t2_country_code, score=raw_score[1], ) header_note_node = header.select_one(".match-header-note") header_note_text = extract_text(header_note_node) aliases1 = [alias for alias in (team1_info.short, team1_info.name) if alias] aliases2 = [alias for alias in (team2_info.short, team2_info.name) if alias] map_actions, picks, bans, remaining = _parse_note_for_picks_bans( header_note_text, aliases1 or [team1_info.name], aliases2 or [team2_info.name], ) return Info( match_id=match_id, teams=(team1_info, team2_info), score=raw_score, status_note=status_note.lower(), best_of=best_of, event=event_name, event_phase=event_phase, date=match_date, time=time_value, patch=patch_text, map_actions=map_actions, picks=picks, bans=bans, remaining=remaining, )