Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,21 @@ For series (or league) details, pass in the series ID (found in a match URL, for
'India tour of Ireland and England 2018'
```

Navigate from a series to its seasons, teams, and matches:

```python
from espncricinfo.series import Series

series = Series(1478874)
season = series.current_season
for team in season.teams:
print(team.name, team.match)
for ref in series.matches:
print(ref.series_id, ref.match_id)
```

`series.season_objects` lists all seasons. `season.match_refs` lists that season's matches, while `series.current_matches` contains only matches whose ESPN status is in progress. These match lists contain lightweight `MatchRef` objects; loading a full `Match` launches Playwright. `team.players` loads player records on demand.

### Tests

**Unit tests** (no network required — uses recorded fixtures):
Expand Down
32 changes: 32 additions & 0 deletions espncricinfo/_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import re

import requests

HEADERS = {"user-agent": "Mozilla/5.0"}


def reference_ids(url, resource):
match = re.search(rf"/leagues/(\d+)/{resource}/(\d+)(?:/|$)", url)
if match is None:
raise ValueError(f"Invalid {resource} reference: {url}")
return int(match.group(1)), int(match.group(2))


def get_json(url, not_found_error):
response = requests.get(url, headers=HEADERS, timeout=30)
if response.status_code == 404:
raise not_found_error(f"Not found: {url}")
response.raise_for_status()
return response.json()


def collection_refs(url, not_found_error):
references = []
page = 1
while True:
page_url = url if page == 1 else f"{url}?page={page}"
data = get_json(page_url, not_found_error)
references.extend(item["$ref"] for item in data.get("items", []))
if page >= data.get("pageCount", 1):
return references
page += 1
8 changes: 8 additions & 0 deletions espncricinfo/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ class NoSeriesError(TypeError):
"""

pass


class NoSeasonError(TypeError):
"""Exception raised if a season does not exist."""


class NoTeamError(TypeError):
"""Exception raised if a team does not exist in a series."""
8 changes: 8 additions & 0 deletions espncricinfo/match_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING

from espncricinfo._core import reference_ids

if TYPE_CHECKING:
from espncricinfo.match import Match

Expand Down Expand Up @@ -48,6 +50,12 @@ def from_dict(cls, d: dict) -> "MatchRef":
"""Construct from a mapping with ``series_id`` and ``match_id`` keys."""
return cls(series_id=d["series_id"], match_id=d["match_id"])

@classmethod
def from_core_event_url(cls, url: str) -> "MatchRef":
"""Construct from a core ESPN event reference."""
series_id, match_id = reference_ids(url, "events")
return cls(series_id=series_id, match_id=match_id)

def to_csv_row(self) -> list:
"""Return ``[str(series_id), str(match_id)]`` for use with ``csv.writer``."""
return [str(self.series_id), str(self.match_id)]
Expand Down
62 changes: 62 additions & 0 deletions espncricinfo/seasons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import datetime

from espncricinfo._core import collection_refs, get_json, reference_ids
from espncricinfo.exceptions import NoSeasonError
from espncricinfo.match_ref import MatchRef
from espncricinfo.teams import Team


class Season:
def __init__(self, season_id, series_id):
self.id = int(season_id)
self.series_id = int(series_id)
self.url = (
"http://core.espnuk.org/v2/sports/cricket/leagues/"
f"{self.series_id}/seasons/{self.id}"
)
self.json = get_json(self.url, NoSeasonError)
self.year = int(self.json.get("year", self.id))
self.name = self.json.get("name")
self.short_name = self.json.get("shortName")
self.start_date = self._parse_date(self.json.get("startDate"))
self.end_date = self._parse_date(self.json.get("endDate"))
self._teams = None
self._match_refs = None
self._series = None

@staticmethod
def _parse_date(value):
return datetime.fromisoformat(value.replace("Z", "+00:00")) if value else None

@classmethod
def from_reference(cls, url):
series_id, season_id = reference_ids(url, "seasons")
return cls(season_id, series_id)

@property
def teams(self):
if self._teams is None:
urls = collection_refs(f"{self.url}/teams", NoSeasonError)
self._teams = [Team.from_reference(url) for url in urls]
return self._teams

@property
def match_refs(self):
if self._match_refs is None:
urls = collection_refs(f"{self.url}/events", NoSeasonError)
self._match_refs = [MatchRef.from_core_event_url(url) for url in urls]
return self._match_refs

@property
def series(self):
if self._series is None:
from espncricinfo.series import Series

self._series = Series(self.series_id)
return self._series

def __str__(self):
return self.name or str(self.year)

def __repr__(self):
return f"Season(id={self.id}, series_id={self.series_id}, name={self.name!r})"
60 changes: 60 additions & 0 deletions espncricinfo/series.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import requests

from espncricinfo._core import collection_refs, get_json
from espncricinfo.exceptions import NoSeriesError
from espncricinfo.match_ref import MatchRef
from espncricinfo.seasons import Season


class Series(object):
Expand All @@ -23,6 +26,10 @@ def __init__(self, series_id):
self.json = self.get_json(self.json_url)
self.seasons = self._get_seasons()
self.years = self._get_years_from_seasons()
self._season_objects = None
self._current_season = None
self._match_refs = None
self._current_matches = None
if self.json:
self.name = self.json["name"]
self.short_name = self.json["shortName"]
Expand Down Expand Up @@ -70,3 +77,56 @@ def _build_events(self):
for event in self.events_json:
events.append(self.get_json(event["$ref"]))
return events

@property
def season_objects(self):
if self._season_objects is None:
self._season_objects = [Season.from_reference(url) for url in self.seasons]
return self._season_objects

@property
def current_season(self):
if self._current_season is None:
reference = (self.json.get("season") or {}).get("$ref")
self._current_season = (
Season.from_reference(reference) if reference else None
)
return self._current_season

@property
def matches(self):
if self._match_refs is None:
if self.seasons:
self._match_refs = list(
dict.fromkeys(
ref
for season in self.season_objects
for ref in season.match_refs
)
)
else:
reference = (self.json.get("events") or {}).get("$ref")
urls = collection_refs(reference or self.events_url, NoSeriesError)
self._match_refs = [MatchRef.from_core_event_url(url) for url in urls]
return self._match_refs

@property
def current_matches(self):
if self._current_matches is None:
active = []
for ref in self.matches:
url = (
"http://core.espnuk.org/v2/sports/cricket/leagues/"
f"{ref.series_id}/events/{ref.match_id}"
)
event = get_json(url, NoSeriesError)
for competition in event.get("competitions", []):
status_url = (competition.get("status") or {}).get("$ref")
if status_url and (
get_json(status_url, NoSeriesError).get("type", {}).get("state")
== "in"
):
active.append(ref)
break
self._current_matches = active
return self._current_matches
62 changes: 62 additions & 0 deletions espncricinfo/teams.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from datetime import datetime

from espncricinfo._core import collection_refs, get_json, reference_ids
from espncricinfo.exceptions import NoTeamError
from espncricinfo.match_ref import MatchRef


class Team:
def __init__(self, team_id, series_id):
self.id = int(team_id)
self.series_id = int(series_id)
self.url = (
"http://core.espnuk.org/v2/sports/cricket/leagues/"
f"{self.series_id}/teams/{self.id}"
)
self.json = get_json(self.url, NoTeamError)
self.name = self.json.get("name")
self.short_name = self.json.get("abbreviation")
self.location = self.json.get("location")
self.nickname = self.json.get("nickname")
self.slug = self.json.get("slug")
self.color = self.json.get("color")
self.logo = next(
(logo["href"] for logo in self.json.get("logos", []) if "href" in logo),
None,
)
self.is_national = self.json.get("isNational")
self.is_active = self.json.get("isActive")
event = self.json.get("event") or {}
self.match_date = (
datetime.fromisoformat(event["date"].replace("Z", "+00:00"))
if event.get("date")
else None
)
self.match = (
MatchRef.from_core_event_url(event["$ref"]) if event.get("$ref") else None
)
self._players = None

@classmethod
def from_reference(cls, url):
series_id, team_id = reference_ids(url, "teams")
return cls(team_id, series_id)

@property
def players(self):
if self._players is None:
from espncricinfo.player import Player

athletes = self.json.get("athletes") or {}
self._players = (
[
Player(int(url.rstrip("/").rsplit("/", 1)[-1]))
for url in collection_refs(athletes["$ref"], NoTeamError)
]
if athletes.get("$ref")
else []
)
return self._players

def __repr__(self):
return f"Team(id={self.id}, series_id={self.series_id}, name={self.name!r})"
Loading
Loading