thesportsdb_client

A Python client for the TheSportsDB API, v1 and v2.

SportsDB (blocking) and AsyncSportsDB (asyncio or trio) share one implementation: every endpoint, typed models, helpers for common tasks, client-side rate limiting, retries, optional caching and de-duplication of identical calls in flight. API keys never appear in errors or cache keys.

 1"""A Python client for the TheSportsDB API, v1 and v2.
 2
 3``SportsDB`` (blocking) and ``AsyncSportsDB`` (asyncio or trio) share one implementation:
 4every endpoint, typed models, helpers for common tasks, client-side rate limiting, retries,
 5optional caching and de-duplication of identical calls in flight. API keys never appear in
 6errors or cache keys.
 7"""
 8
 9from ._async._transport import AsyncTransport, HttpxAsyncTransport, Response
10from ._async.client import AsyncSportsDB
11from ._sync._transport import HttpxTransport, Transport
12from ._sync.client import SportsDB
13from .cache import CachePolicy, Freshness, InMemoryResponseCache, ResponseCache
14from .config import FREE_API_KEY, FREE_API_KEYS, Config
15from .errors import (
16    ApiMessageError,
17    HTTPStatusError,
18    InvalidApiKeyError,
19    NetworkError,
20    PremiumRequiredError,
21    RateLimitError,
22    ResponseParseError,
23    SportsDBError,
24)
25from .events import RequestEvent
26from .models import (
27    ApiRecord,
28    Contract,
29    Country,
30    Equipment,
31    Event,
32    EventResult,
33    EventStat,
34    EventStatus,
35    FormerTeam,
36    Honour,
37    ImageSize,
38    League,
39    LeagueRef,
40    LineupEntry,
41    LiveScore,
42    Milestone,
43    Player,
44    PlayerExternalIds,
45    PlayerStat,
46    RoundStage,
47    Season,
48    SeasonPoster,
49    Socials,
50    Sport,
51    Standing,
52    Team,
53    TimelineEntry,
54    TvListing,
55    Venue,
56    sized,
57)
58
59__version__ = "0.1.0"
60
61__all__ = [
62    "AsyncSportsDB", "SportsDB", "Config", "FREE_API_KEY", "FREE_API_KEYS",
63    "AsyncTransport", "Transport", "HttpxAsyncTransport", "HttpxTransport", "Response",
64    "CachePolicy", "Freshness", "InMemoryResponseCache", "ResponseCache", "RequestEvent",
65    "SportsDBError", "InvalidApiKeyError", "PremiumRequiredError", "RateLimitError", "HTTPStatusError",
66    "ResponseParseError", "ApiMessageError", "NetworkError",
67    "ApiRecord", "Contract", "Country", "Equipment", "Event", "EventResult", "EventStat", "EventStatus",
68    "FormerTeam", "Honour", "ImageSize", "League", "LeagueRef", "LineupEntry", "LiveScore", "Milestone",
69    "Player", "PlayerExternalIds", "PlayerStat", "RoundStage", "Season", "SeasonPoster", "Socials", "Sport", "Standing", "Team",
70    "TimelineEntry", "TvListing", "Venue", "sized",
71]
class AsyncSportsDB:
24class AsyncSportsDB:
25    """The asynchronous TheSportsDB client.
26
27    .. code-block:: python
28
29        async with AsyncSportsDB() as db:                    # free key 123: v1 only
30            arsenal = await db.v1.lookup.team(133604)
31        async with AsyncSportsDB(api_key=key) as db:         # premium: v1 and v2
32            tv = await db.v2.tv.country("Canada")
33
34    One client is safe to share across tasks; its rate limiter, cache and de-duplication cover
35    every call made through it. Create one per key and reuse it.
36    """
37
38    def __init__(
39        self,
40        api_key: str = FREE_API_KEY,
41        *,
42        transport: AsyncTransport | None = None,
43        requests_per_minute: int | None = None,
44        max_retries: int = 2,
45        retry_backoff: float = 0.5,
46        retry_on_rate_limit: bool = True,
47        rate_limit_wait: float = 60.0,
48        cache: ResponseCache | None = None,
49        cache_policy: CachePolicy | None = None,
50        timeout: float = 30.0,
51        deduplicate_requests: bool = True,
52        request_listener: Callable[[RequestEvent], None] | None = None,
53        base_url: str = "https://www.thesportsdb.com",
54        user_agent: str = "thesportsdb-client-python",
55    ) -> None:
56        options: dict[str, Any] = dict(
57            api_key=api_key, requests_per_minute=requests_per_minute, max_retries=max_retries,
58            retry_backoff=retry_backoff, retry_on_rate_limit=retry_on_rate_limit, rate_limit_wait=rate_limit_wait,
59            cache=cache, timeout=timeout, deduplicate_requests=deduplicate_requests,
60            request_listener=request_listener, base_url=base_url, user_agent=user_agent,
61        )
62        if cache_policy is not None:
63            options["cache_policy"] = cache_policy
64        self.config = Config(**options)
65        self._owns_transport = transport is None
66        self._transport = transport or HttpxAsyncTransport(timeout=timeout)
67        requester = AsyncRequester(self.config, self._transport)
68        self.v1 = AsyncV1Api(requester)
69        """The v1 API (free and premium keys)."""
70        self.v2 = AsyncV2Api(requester)
71        """The v2 API (premium keys only)."""
72        self.helpers = AsyncHelpers(self)
73        """Common tasks in one call, using v2 or v1 depending on the key."""
74
75    async def is_premium_key(self) -> bool:
76        """Whether v2 accepts the key. One call (v2 ``lookup/league/4328``) unless it's a free key."""
77        if self.config.is_free_key:
78            return False
79        try:
80            await self.v2.lookup.league(4328)
81            return True
82        except InvalidApiKeyError:
83            return False
84
85    async def aclose(self) -> None:
86        """Closes the HTTP connections the client opened (not a transport you passed in)."""
87        if self._owns_transport:
88            await self._transport.aclose()
89
90    async def __aenter__(self) -> AsyncSportsDB:
91        return self
92
93    async def __aexit__(
94        self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
95    ) -> None:
96        await self.aclose()

The asynchronous TheSportsDB client.

async with AsyncSportsDB() as db:                    # free key 123: v1 only
    arsenal = await db.v1.lookup.team(133604)
async with AsyncSportsDB(api_key=key) as db:         # premium: v1 and v2
    tv = await db.v2.tv.country("Canada")

One client is safe to share across tasks; its rate limiter, cache and de-duplication cover every call made through it. Create one per key and reuse it.

AsyncSportsDB( api_key: str = '123', *, transport: AsyncTransport | None = None, requests_per_minute: int | None = None, max_retries: int = 2, retry_backoff: float = 0.5, retry_on_rate_limit: bool = True, rate_limit_wait: float = 60.0, cache: ResponseCache | None = None, cache_policy: CachePolicy | None = None, timeout: float = 30.0, deduplicate_requests: bool = True, request_listener: Callable[[RequestEvent], None] | None = None, base_url: str = 'https://www.thesportsdb.com', user_agent: str = 'thesportsdb-client-python')
38    def __init__(
39        self,
40        api_key: str = FREE_API_KEY,
41        *,
42        transport: AsyncTransport | None = None,
43        requests_per_minute: int | None = None,
44        max_retries: int = 2,
45        retry_backoff: float = 0.5,
46        retry_on_rate_limit: bool = True,
47        rate_limit_wait: float = 60.0,
48        cache: ResponseCache | None = None,
49        cache_policy: CachePolicy | None = None,
50        timeout: float = 30.0,
51        deduplicate_requests: bool = True,
52        request_listener: Callable[[RequestEvent], None] | None = None,
53        base_url: str = "https://www.thesportsdb.com",
54        user_agent: str = "thesportsdb-client-python",
55    ) -> None:
56        options: dict[str, Any] = dict(
57            api_key=api_key, requests_per_minute=requests_per_minute, max_retries=max_retries,
58            retry_backoff=retry_backoff, retry_on_rate_limit=retry_on_rate_limit, rate_limit_wait=rate_limit_wait,
59            cache=cache, timeout=timeout, deduplicate_requests=deduplicate_requests,
60            request_listener=request_listener, base_url=base_url, user_agent=user_agent,
61        )
62        if cache_policy is not None:
63            options["cache_policy"] = cache_policy
64        self.config = Config(**options)
65        self._owns_transport = transport is None
66        self._transport = transport or HttpxAsyncTransport(timeout=timeout)
67        requester = AsyncRequester(self.config, self._transport)
68        self.v1 = AsyncV1Api(requester)
69        """The v1 API (free and premium keys)."""
70        self.v2 = AsyncV2Api(requester)
71        """The v2 API (premium keys only)."""
72        self.helpers = AsyncHelpers(self)
73        """Common tasks in one call, using v2 or v1 depending on the key."""
config
v1

The v1 API (free and premium keys).

v2

The v2 API (premium keys only).

helpers

Common tasks in one call, using v2 or v1 depending on the key.

async def is_premium_key(self) -> bool:
75    async def is_premium_key(self) -> bool:
76        """Whether v2 accepts the key. One call (v2 ``lookup/league/4328``) unless it's a free key."""
77        if self.config.is_free_key:
78            return False
79        try:
80            await self.v2.lookup.league(4328)
81            return True
82        except InvalidApiKeyError:
83            return False

Whether v2 accepts the key. One call (v2 lookup/league/4328) unless it's a free key.

async def aclose(self) -> None:
85    async def aclose(self) -> None:
86        """Closes the HTTP connections the client opened (not a transport you passed in)."""
87        if self._owns_transport:
88            await self._transport.aclose()

Closes the HTTP connections the client opened (not a transport you passed in).

class SportsDB:
25class SportsDB:
26    """The blocking TheSportsDB client.
27
28    .. code-block:: python
29
30        with SportsDB() as db:                    # free key 123: v1 only
31            arsenal = db.v1.lookup.team(133604)
32        with SportsDB(api_key=key) as db:         # premium: v1 and v2
33            tv = db.v2.tv.country("Canada")
34
35    One client is safe to share across tasks; its rate limiter, cache and de-duplication cover
36    every call made through it. Create one per key and reuse it.
37    """
38
39    def __init__(
40        self,
41        api_key: str = FREE_API_KEY,
42        *,
43        transport: Transport | None = None,
44        requests_per_minute: int | None = None,
45        max_retries: int = 2,
46        retry_backoff: float = 0.5,
47        retry_on_rate_limit: bool = True,
48        rate_limit_wait: float = 60.0,
49        cache: ResponseCache | None = None,
50        cache_policy: CachePolicy | None = None,
51        timeout: float = 30.0,
52        deduplicate_requests: bool = True,
53        request_listener: Callable[[RequestEvent], None] | None = None,
54        base_url: str = "https://www.thesportsdb.com",
55        user_agent: str = "thesportsdb-client-python",
56    ) -> None:
57        options: dict[str, Any] = dict(
58            api_key=api_key, requests_per_minute=requests_per_minute, max_retries=max_retries,
59            retry_backoff=retry_backoff, retry_on_rate_limit=retry_on_rate_limit, rate_limit_wait=rate_limit_wait,
60            cache=cache, timeout=timeout, deduplicate_requests=deduplicate_requests,
61            request_listener=request_listener, base_url=base_url, user_agent=user_agent,
62        )
63        if cache_policy is not None:
64            options["cache_policy"] = cache_policy
65        self.config = Config(**options)
66        self._owns_transport = transport is None
67        self._transport = transport or HttpxTransport(timeout=timeout)
68        requester = Requester(self.config, self._transport)
69        self.v1 = V1Api(requester)
70        """The v1 API (free and premium keys)."""
71        self.v2 = V2Api(requester)
72        """The v2 API (premium keys only)."""
73        self.helpers = Helpers(self)
74        """Common tasks in one call, using v2 or v1 depending on the key."""
75
76    def is_premium_key(self) -> bool:
77        """Whether v2 accepts the key. One call (v2 ``lookup/league/4328``) unless it's a free key."""
78        if self.config.is_free_key:
79            return False
80        try:
81            self.v2.lookup.league(4328)
82            return True
83        except InvalidApiKeyError:
84            return False
85
86    def close(self) -> None:
87        """Closes the HTTP connections the client opened (not a transport you passed in)."""
88        if self._owns_transport:
89            self._transport.close()
90
91    def __enter__(self) -> SportsDB:
92        return self
93
94    def __exit__(
95        self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
96    ) -> None:
97        self.close()

The blocking TheSportsDB client.

with SportsDB() as db:                    # free key 123: v1 only
    arsenal = db.v1.lookup.team(133604)
with SportsDB(api_key=key) as db:         # premium: v1 and v2
    tv = db.v2.tv.country("Canada")

One client is safe to share across tasks; its rate limiter, cache and de-duplication cover every call made through it. Create one per key and reuse it.

SportsDB( api_key: str = '123', *, transport: Transport | None = None, requests_per_minute: int | None = None, max_retries: int = 2, retry_backoff: float = 0.5, retry_on_rate_limit: bool = True, rate_limit_wait: float = 60.0, cache: ResponseCache | None = None, cache_policy: CachePolicy | None = None, timeout: float = 30.0, deduplicate_requests: bool = True, request_listener: Callable[[RequestEvent], None] | None = None, base_url: str = 'https://www.thesportsdb.com', user_agent: str = 'thesportsdb-client-python')
39    def __init__(
40        self,
41        api_key: str = FREE_API_KEY,
42        *,
43        transport: Transport | None = None,
44        requests_per_minute: int | None = None,
45        max_retries: int = 2,
46        retry_backoff: float = 0.5,
47        retry_on_rate_limit: bool = True,
48        rate_limit_wait: float = 60.0,
49        cache: ResponseCache | None = None,
50        cache_policy: CachePolicy | None = None,
51        timeout: float = 30.0,
52        deduplicate_requests: bool = True,
53        request_listener: Callable[[RequestEvent], None] | None = None,
54        base_url: str = "https://www.thesportsdb.com",
55        user_agent: str = "thesportsdb-client-python",
56    ) -> None:
57        options: dict[str, Any] = dict(
58            api_key=api_key, requests_per_minute=requests_per_minute, max_retries=max_retries,
59            retry_backoff=retry_backoff, retry_on_rate_limit=retry_on_rate_limit, rate_limit_wait=rate_limit_wait,
60            cache=cache, timeout=timeout, deduplicate_requests=deduplicate_requests,
61            request_listener=request_listener, base_url=base_url, user_agent=user_agent,
62        )
63        if cache_policy is not None:
64            options["cache_policy"] = cache_policy
65        self.config = Config(**options)
66        self._owns_transport = transport is None
67        self._transport = transport or HttpxTransport(timeout=timeout)
68        requester = Requester(self.config, self._transport)
69        self.v1 = V1Api(requester)
70        """The v1 API (free and premium keys)."""
71        self.v2 = V2Api(requester)
72        """The v2 API (premium keys only)."""
73        self.helpers = Helpers(self)
74        """Common tasks in one call, using v2 or v1 depending on the key."""
config
v1

The v1 API (free and premium keys).

v2

The v2 API (premium keys only).

helpers

Common tasks in one call, using v2 or v1 depending on the key.

def is_premium_key(self) -> bool:
76    def is_premium_key(self) -> bool:
77        """Whether v2 accepts the key. One call (v2 ``lookup/league/4328``) unless it's a free key."""
78        if self.config.is_free_key:
79            return False
80        try:
81            self.v2.lookup.league(4328)
82            return True
83        except InvalidApiKeyError:
84            return False

Whether v2 accepts the key. One call (v2 lookup/league/4328) unless it's a free key.

def close(self) -> None:
86    def close(self) -> None:
87        """Closes the HTTP connections the client opened (not a transport you passed in)."""
88        if self._owns_transport:
89            self._transport.close()

Closes the HTTP connections the client opened (not a transport you passed in).

@dataclass(frozen=True, kw_only=True)
class Config:
19@dataclass(frozen=True, kw_only=True)
20class Config:
21    api_key: str = FREE_API_KEY
22    requests_per_minute: int | None = None
23    """Client-side limit shared by v1 and v2. None: 30 for a free key, 100 otherwise. 0 turns it off."""
24    max_retries: int = 2
25    """Retries for network errors and HTTP 5xx, with exponential backoff."""
26    retry_backoff: float = 0.5
27    """First backoff, in seconds; doubles on every retry."""
28    retry_on_rate_limit: bool = True
29    """On HTTP 429, wait (Retry-After, or rate_limit_wait) and retry once."""
30    rate_limit_wait: float = 60.0
31    cache: ResponseCache | None = None
32    cache_policy: CachePolicy = field(default_factory=CachePolicy)
33    timeout: float = 30.0
34    """Seconds before a request gives up (connect and read)."""
35    deduplicate_requests: bool = True
36    """Identical calls made at the same time share one HTTP request."""
37    request_listener: Callable[[RequestEvent], None] | None = None
38    """Called once per call, after it finishes. Exceptions it raises are ignored."""
39    base_url: str = "https://www.thesportsdb.com"
40    user_agent: str = "thesportsdb-client-python"
41
42    @property
43    def is_free_key(self) -> bool:
44        return self.api_key in FREE_API_KEYS
45
46    @property
47    def effective_requests_per_minute(self) -> int:
48        if self.requests_per_minute is not None:
49            return self.requests_per_minute
50        return 30 if self.is_free_key else 100
Config( *, api_key: str = '123', requests_per_minute: int | None = None, max_retries: int = 2, retry_backoff: float = 0.5, retry_on_rate_limit: bool = True, rate_limit_wait: float = 60.0, cache: ResponseCache | None = None, cache_policy: CachePolicy = <factory>, timeout: float = 30.0, deduplicate_requests: bool = True, request_listener: Callable[[RequestEvent], None] | None = None, base_url: str = 'https://www.thesportsdb.com', user_agent: str = 'thesportsdb-client-python')
api_key: str = '123'
requests_per_minute: int | None = None

Client-side limit shared by v1 and v2. None: 30 for a free key, 100 otherwise. 0 turns it off.

max_retries: int = 2

Retries for network errors and HTTP 5xx, with exponential backoff.

retry_backoff: float = 0.5

First backoff, in seconds; doubles on every retry.

retry_on_rate_limit: bool = True

On HTTP 429, wait (Retry-After, or rate_limit_wait) and retry once.

rate_limit_wait: float = 60.0
cache: ResponseCache | None = None
cache_policy: CachePolicy
timeout: float = 30.0

Seconds before a request gives up (connect and read).

deduplicate_requests: bool = True

Identical calls made at the same time share one HTTP request.

request_listener: Callable[[RequestEvent], None] | None = None

Called once per call, after it finishes. Exceptions it raises are ignored.

base_url: str = 'https://www.thesportsdb.com'
user_agent: str = 'thesportsdb-client-python'
is_free_key: bool
42    @property
43    def is_free_key(self) -> bool:
44        return self.api_key in FREE_API_KEYS
effective_requests_per_minute: int
46    @property
47    def effective_requests_per_minute(self) -> int:
48        if self.requests_per_minute is not None:
49            return self.requests_per_minute
50        return 30 if self.is_free_key else 100
FREE_API_KEY = '123'
FREE_API_KEYS = frozenset({'3', '123'})
class AsyncTransport(typing.Protocol):
26class AsyncTransport(Protocol):
27    """Performs GET requests. Raise ``OSError`` (e.g. ``ConnectionError``) for network failures;
28    return every HTTP status, including 4xx and 5xx, as a ``Response``."""
29
30    async def get(self, url: str, headers: Mapping[str, str]) -> Response: ...
31
32    async def aclose(self) -> None: ...

Performs GET requests. Raise OSError (e.g. ConnectionError) for network failures; return every HTTP status, including 4xx and 5xx, as a Response.

AsyncTransport(*args, **kwargs)
1874def _no_init_or_replace_init(self, *args, **kwargs):
1875    cls = type(self)
1876
1877    if cls._is_protocol:
1878        raise TypeError('Protocols cannot be instantiated')
1879
1880    # Already using a custom `__init__`. No need to calculate correct
1881    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1882    if cls.__init__ is not _no_init_or_replace_init:
1883        return
1884
1885    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1886    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1887    # searches for a proper new `__init__` in the MRO. The new `__init__`
1888    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1889    # instantiation of the protocol subclass will thus use the new
1890    # `__init__` and no longer call `_no_init_or_replace_init`.
1891    for base in cls.__mro__:
1892        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1893        if init is not _no_init_or_replace_init:
1894            cls.__init__ = init
1895            break
1896    else:
1897        # should not happen
1898        cls.__init__ = object.__init__
1899
1900    cls.__init__(self, *args, **kwargs)
async def get( self, url: str, headers: Mapping[str, str]) -> Response:
30    async def get(self, url: str, headers: Mapping[str, str]) -> Response: ...
async def aclose(self) -> None:
32    async def aclose(self) -> None: ...
class Transport(typing.Protocol):
27class Transport(Protocol):
28    """Performs GET requests. Raise ``OSError`` (e.g. ``ConnectionError``) for network failures;
29    return every HTTP status, including 4xx and 5xx, as a ``Response``."""
30
31    def get(self, url: str, headers: Mapping[str, str]) -> Response: ...
32
33    def close(self) -> None: ...

Performs GET requests. Raise OSError (e.g. ConnectionError) for network failures; return every HTTP status, including 4xx and 5xx, as a Response.

Transport(*args, **kwargs)
1874def _no_init_or_replace_init(self, *args, **kwargs):
1875    cls = type(self)
1876
1877    if cls._is_protocol:
1878        raise TypeError('Protocols cannot be instantiated')
1879
1880    # Already using a custom `__init__`. No need to calculate correct
1881    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1882    if cls.__init__ is not _no_init_or_replace_init:
1883        return
1884
1885    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1886    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1887    # searches for a proper new `__init__` in the MRO. The new `__init__`
1888    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1889    # instantiation of the protocol subclass will thus use the new
1890    # `__init__` and no longer call `_no_init_or_replace_init`.
1891    for base in cls.__mro__:
1892        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1893        if init is not _no_init_or_replace_init:
1894            cls.__init__ = init
1895            break
1896    else:
1897        # should not happen
1898        cls.__init__ = object.__init__
1899
1900    cls.__init__(self, *args, **kwargs)
def get( self, url: str, headers: Mapping[str, str]) -> Response:
31    def get(self, url: str, headers: Mapping[str, str]) -> Response: ...
def close(self) -> None:
33    def close(self) -> None: ...
class HttpxAsyncTransport:
35class HttpxAsyncTransport:
36    """The default transport, on httpx. Pass your own ``httpx.AsyncClient`` to share connections."""
37
38    def __init__(self, client: httpx.AsyncClient | None = None, timeout: float = 30.0) -> None:
39        self._owned = client is None
40        self._client = client or httpx.AsyncClient(timeout=timeout)
41
42    async def get(self, url: str, headers: Mapping[str, str]) -> Response:
43        try:
44            r = await self._client.get(url, headers=dict(headers))
45        except httpx.TransportError as e:
46            raise ConnectionError(str(e) or type(e).__name__) from e
47        return Response(r.status_code, r.text, dict(r.headers))
48
49    async def aclose(self) -> None:
50        if self._owned:
51            await self._client.aclose()

The default transport, on httpx. Pass your own httpx.AsyncClient to share connections.

HttpxAsyncTransport(client: httpx.AsyncClient | None = None, timeout: float = 30.0)
38    def __init__(self, client: httpx.AsyncClient | None = None, timeout: float = 30.0) -> None:
39        self._owned = client is None
40        self._client = client or httpx.AsyncClient(timeout=timeout)
async def get( self, url: str, headers: Mapping[str, str]) -> Response:
42    async def get(self, url: str, headers: Mapping[str, str]) -> Response:
43        try:
44            r = await self._client.get(url, headers=dict(headers))
45        except httpx.TransportError as e:
46            raise ConnectionError(str(e) or type(e).__name__) from e
47        return Response(r.status_code, r.text, dict(r.headers))
async def aclose(self) -> None:
49    async def aclose(self) -> None:
50        if self._owned:
51            await self._client.aclose()
class HttpxTransport:
36class HttpxTransport:
37    """The default transport, on httpx. Pass your own ``httpx.Client`` to share connections."""
38
39    def __init__(self, client: httpx.Client | None = None, timeout: float = 30.0) -> None:
40        self._owned = client is None
41        self._client = client or httpx.Client(timeout=timeout)
42
43    def get(self, url: str, headers: Mapping[str, str]) -> Response:
44        try:
45            r = self._client.get(url, headers=dict(headers))
46        except httpx.TransportError as e:
47            raise ConnectionError(str(e) or type(e).__name__) from e
48        return Response(r.status_code, r.text, dict(r.headers))
49
50    def close(self) -> None:
51        if self._owned:
52            self._client.close()

The default transport, on httpx. Pass your own httpx.Client to share connections.

HttpxTransport(client: httpx.Client | None = None, timeout: float = 30.0)
39    def __init__(self, client: httpx.Client | None = None, timeout: float = 30.0) -> None:
40        self._owned = client is None
41        self._client = client or httpx.Client(timeout=timeout)
def get( self, url: str, headers: Mapping[str, str]) -> Response:
43    def get(self, url: str, headers: Mapping[str, str]) -> Response:
44        try:
45            r = self._client.get(url, headers=dict(headers))
46        except httpx.TransportError as e:
47            raise ConnectionError(str(e) or type(e).__name__) from e
48        return Response(r.status_code, r.text, dict(r.headers))
def close(self) -> None:
50    def close(self) -> None:
51        if self._owned:
52            self._client.close()
@dataclass(frozen=True)
class Response:
16@dataclass(frozen=True)
17class Response:
18    status: int
19    body: str
20    headers: Mapping[str, str] = field(default_factory=dict)
21
22    def header(self, name: str) -> str | None:
23        return next((v for k, v in self.headers.items() if k.lower() == name.lower()), None)
Response(status: int, body: str, headers: Mapping[str, str] = <factory>)
status: int
body: str
headers: Mapping[str, str]
def header(self, name: str) -> str | None:
22    def header(self, name: str) -> str | None:
23        return next((v for k, v in self.headers.items() if k.lower() == name.lower()), None)
@dataclass(frozen=True)
class CachePolicy:
24@dataclass(frozen=True)
25class CachePolicy:
26    """Time-to-live in seconds for each Freshness. Zero or less means don't cache."""
27
28    static: float = 7 * 24 * 3600
29    slow: float = 24 * 3600
30    medium: float = 3600
31    live: float = 0
32
33    def ttl(self, freshness: Freshness) -> float:
34        return {Freshness.STATIC: self.static, Freshness.SLOW: self.slow,
35                Freshness.MEDIUM: self.medium, Freshness.LIVE: self.live}[freshness]

Time-to-live in seconds for each Freshness. Zero or less means don't cache.

CachePolicy( static: float = 604800, slow: float = 86400, medium: float = 3600, live: float = 0)
static: float = 604800
slow: float = 86400
medium: float = 3600
live: float = 0
def ttl(self, freshness: Freshness) -> float:
33    def ttl(self, freshness: Freshness) -> float:
34        return {Freshness.STATIC: self.static, Freshness.SLOW: self.slow,
35                Freshness.MEDIUM: self.medium, Freshness.LIVE: self.live}[freshness]
class Freshness(enum.Enum):
15class Freshness(Enum):
16    """How quickly an endpoint's data changes. Every endpoint is tagged with one."""
17
18    STATIC = "static"  # sports, countries, the league list
19    SLOW = "slow"  # teams, players, venues, seasons
20    MEDIUM = "medium"  # schedules, standings, TV listings, highlights
21    LIVE = "live"  # live scores

How quickly an endpoint's data changes. Every endpoint is tagged with one.

STATIC = <Freshness.STATIC: 'static'>
SLOW = <Freshness.SLOW: 'slow'>
MEDIUM = <Freshness.MEDIUM: 'medium'>
LIVE = <Freshness.LIVE: 'live'>
class InMemoryResponseCache:
46class InMemoryResponseCache:
47    """A bounded, thread-safe, in-memory LRU cache with per-entry expiry."""
48
49    def __init__(self, max_entries: int = 1000, clock: Callable[[], float] = time.monotonic) -> None:
50        self._max = max_entries
51        self._clock = clock
52        self._lock = threading.Lock()
53        self._entries: OrderedDict[str, tuple[str, float]] = OrderedDict()
54
55    def get(self, key: str) -> str | None:
56        with self._lock:
57            entry = self._entries.get(key)
58            if entry is None:
59                return None
60            body, expires = entry
61            if expires <= self._clock():
62                del self._entries[key]
63                return None
64            self._entries.move_to_end(key)
65            return body
66
67    def put(self, key: str, body: str, ttl: float) -> None:
68        if ttl <= 0:
69            return
70        with self._lock:
71            self._entries[key] = (body, self._clock() + ttl)
72            self._entries.move_to_end(key)
73            while len(self._entries) > self._max:
74                self._entries.popitem(last=False)
75
76    def clear(self) -> None:
77        with self._lock:
78            self._entries.clear()
79
80    def __len__(self) -> int:
81        with self._lock:
82            return len(self._entries)

A bounded, thread-safe, in-memory LRU cache with per-entry expiry.

InMemoryResponseCache( max_entries: int = 1000, clock: Callable[[], float] = <built-in function monotonic>)
49    def __init__(self, max_entries: int = 1000, clock: Callable[[], float] = time.monotonic) -> None:
50        self._max = max_entries
51        self._clock = clock
52        self._lock = threading.Lock()
53        self._entries: OrderedDict[str, tuple[str, float]] = OrderedDict()
def get(self, key: str) -> str | None:
55    def get(self, key: str) -> str | None:
56        with self._lock:
57            entry = self._entries.get(key)
58            if entry is None:
59                return None
60            body, expires = entry
61            if expires <= self._clock():
62                del self._entries[key]
63                return None
64            self._entries.move_to_end(key)
65            return body
def put(self, key: str, body: str, ttl: float) -> None:
67    def put(self, key: str, body: str, ttl: float) -> None:
68        if ttl <= 0:
69            return
70        with self._lock:
71            self._entries[key] = (body, self._clock() + ttl)
72            self._entries.move_to_end(key)
73            while len(self._entries) > self._max:
74                self._entries.popitem(last=False)
def clear(self) -> None:
76    def clear(self) -> None:
77        with self._lock:
78            self._entries.clear()
class ResponseCache(typing.Protocol):
38class ResponseCache(Protocol):
39    """Stores raw response bodies. Keys never contain an API key. Implement it for Redis, disk, etc."""
40
41    def get(self, key: str) -> str | None: ...
42
43    def put(self, key: str, body: str, ttl: float) -> None: ...

Stores raw response bodies. Keys never contain an API key. Implement it for Redis, disk, etc.

ResponseCache(*args, **kwargs)
1874def _no_init_or_replace_init(self, *args, **kwargs):
1875    cls = type(self)
1876
1877    if cls._is_protocol:
1878        raise TypeError('Protocols cannot be instantiated')
1879
1880    # Already using a custom `__init__`. No need to calculate correct
1881    # `__init__` to call. This can lead to RecursionError. See bpo-45121.
1882    if cls.__init__ is not _no_init_or_replace_init:
1883        return
1884
1885    # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`.
1886    # The first instantiation of the subclass will call `_no_init_or_replace_init` which
1887    # searches for a proper new `__init__` in the MRO. The new `__init__`
1888    # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent
1889    # instantiation of the protocol subclass will thus use the new
1890    # `__init__` and no longer call `_no_init_or_replace_init`.
1891    for base in cls.__mro__:
1892        init = base.__dict__.get('__init__', _no_init_or_replace_init)
1893        if init is not _no_init_or_replace_init:
1894            cls.__init__ = init
1895            break
1896    else:
1897        # should not happen
1898        cls.__init__ = object.__init__
1899
1900    cls.__init__(self, *args, **kwargs)
def get(self, key: str) -> str | None:
41    def get(self, key: str) -> str | None: ...
def put(self, key: str, body: str, ttl: float) -> None:
43    def put(self, key: str, body: str, ttl: float) -> None: ...
@dataclass(frozen=True)
class RequestEvent:
 9@dataclass(frozen=True)
10class RequestEvent:
11    """One finished API call, passed to ``request_listener``."""
12
13    url: str
14    """The URL with any v1 key replaced by ``***``. Safe to log."""
15    status: int | None
16    """The last HTTP status; None if no response arrived or the result came from the cache."""
17    attempts: int
18    """HTTP requests made, counting retries; 0 when served by the cache or another caller's request."""
19    from_cache: bool
20    shared: bool
21    """True when an identical call already in flight supplied the result."""
22    duration: float
23    """Seconds for the whole call, including rate-limit waits and retries."""
24    error: BaseException | None

One finished API call, passed to request_listener.

RequestEvent( url: str, status: int | None, attempts: int, from_cache: bool, shared: bool, duration: float, error: BaseException | None)
url: str

The URL with any v1 key replaced by ***. Safe to log.

status: int | None

The last HTTP status; None if no response arrived or the result came from the cache.

attempts: int

HTTP requests made, counting retries; 0 when served by the cache or another caller's request.

from_cache: bool
shared: bool

True when an identical call already in flight supplied the result.

duration: float

Seconds for the whole call, including rate-limit waits and retries.

error: BaseException | None
class SportsDBError(builtins.Exception):
11class SportsDBError(Exception):
12    """Base class for every error this library raises."""

Base class for every error this library raises.

class InvalidApiKeyError(thesportsdb_client.SportsDBError):
15class InvalidApiKeyError(SportsDBError):
16    """The API rejected the key (HTTP 400 ``Invalid Premium API key``). Only ``123``, ``3`` and paid keys work."""

The API rejected the key (HTTP 400 Invalid Premium API key). Only 123, 3 and paid keys work.

class PremiumRequiredError(thesportsdb_client.SportsDBError):
19class PremiumRequiredError(SportsDBError):
20    """A v2 endpoint was called with a free key. v2 accepts premium keys only. Raised before any request."""

A v2 endpoint was called with a free key. v2 accepts premium keys only. Raised before any request.

class RateLimitError(thesportsdb_client.SportsDBError):
23class RateLimitError(SportsDBError):
24    """HTTP 429 after the retry was used up or disabled. ``retry_after`` is the server's wait in seconds, if given."""
25
26    def __init__(self, message: str, retry_after: float | None) -> None:
27        super().__init__(message)
28        self.retry_after = retry_after

HTTP 429 after the retry was used up or disabled. retry_after is the server's wait in seconds, if given.

RateLimitError(message: str, retry_after: float | None)
26    def __init__(self, message: str, retry_after: float | None) -> None:
27        super().__init__(message)
28        self.retry_after = retry_after
retry_after
class HTTPStatusError(thesportsdb_client.SportsDBError):
31class HTTPStatusError(SportsDBError):
32    """Any other non-2xx HTTP response, after retries for 5xx."""
33
34    def __init__(self, message: str, status: int, body_snippet: str) -> None:
35        super().__init__(message)
36        self.status = status
37        self.body_snippet = body_snippet

Any other non-2xx HTTP response, after retries for 5xx.

HTTPStatusError(message: str, status: int, body_snippet: str)
34    def __init__(self, message: str, status: int, body_snippet: str) -> None:
35        super().__init__(message)
36        self.status = status
37        self.body_snippet = body_snippet
status
body_snippet
class ResponseParseError(thesportsdb_client.SportsDBError):
40class ResponseParseError(SportsDBError):
41    """The body was not a TheSportsDB envelope (e.g. an HTML error page)."""

The body was not a TheSportsDB envelope (e.g. an HTML error page).

class ApiMessageError(thesportsdb_client.SportsDBError):
44class ApiMessageError(SportsDBError):
45    """The API answered with a message instead of data.
46
47    Either an unrecognised ``{"Message": ...}`` body, or a rejected parameter, which v1
48    reports as text where the records belong: ``{"seasons": "Invalid League ID passed"}``.
49    """
50
51    def __init__(self, message: str, api_message: str) -> None:
52        super().__init__(message)
53        self.api_message = api_message

The API answered with a message instead of data.

Either an unrecognised {"Message": ...} body, or a rejected parameter, which v1 reports as text where the records belong: {"seasons": "Invalid League ID passed"}.

ApiMessageError(message: str, api_message: str)
51    def __init__(self, message: str, api_message: str) -> None:
52        super().__init__(message)
53        self.api_message = api_message
api_message
class NetworkError(thesportsdb_client.SportsDBError):
56class NetworkError(SportsDBError):
57    """No HTTP response arrived (DNS, connection, timeout), after retries."""

No HTTP response arrived (DNS, connection, timeout), after retries.

@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class ApiRecord:
30@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
31class ApiRecord:
32    """Base class of every record ([Team], [Event], ...)."""
33
34    raw: Mapping[str, str | None] = field(default_factory=lambda: MappingProxyType({}))
35    """Every field as the API sent it (blank strings as None). Use it for unmodelled fields: ``team.raw["strKeywords"]``."""
36
37    _summary: ClassVar[tuple[str, ...]] = ()
38
39    def __eq__(self, other: object) -> bool:
40        return type(self) is type(other) and isinstance(other, ApiRecord) and dict(self.raw) == dict(other.raw)
41
42    def __hash__(self) -> int:
43        return hash((type(self), frozenset(self.raw.items())))
44
45    def __repr__(self) -> str:
46        parts = ", ".join(f"{name}={getattr(self, name)!r}" for name in self._summary)
47        return f"{type(self).__name__}({parts})"
48
49    @classmethod
50    def _from(cls: type[R], raw: Mapping[str, str | None]) -> R:
51        values = cls._read(Rec(raw))
52        return cls(raw=MappingProxyType(dict(raw)), **values)
53
54    @classmethod
55    def _read(cls, r: Rec) -> dict[str, Any]:
56        raise NotImplementedError

Base class of every record ([Team], [Event], ...).

ApiRecord(*, raw: Mapping[str, str | None] = <factory>)
raw: Mapping[str, str | None]

Every field as the API sent it (blank strings as None). Use it for unmodelled fields: team.raw["strKeywords"].

@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Contract(thesportsdb_client.ApiRecord):
528@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
529class Contract(ApiRecord):
530    """A player's contract."""
531
532    _summary = ("player", "team", "year_start", "year_end")
533    id: int | None  # id
534    player_id: int | None  # idPlayer
535    player: str | None  # strPlayer
536    team_id: int | None  # idTeam
537    team: str | None  # strTeam
538    badge: str | None  # strBadge
539    year_start: int | None  # strYearStart
540    year_end: int | None  # strYearEnd
541    wage: str | None  # strWage, as text
542    sport: str | None  # strSport
543
544    @classmethod
545    def _read(cls, r: Rec) -> dict[str, Any]:
546        return dict(id=r.id_("id"), player_id=r.id_("idPlayer"), player=r.s("strPlayer"), team_id=r.id_("idTeam"),
547                    team=r.s("strTeam"), badge=r.s("strBadge"), year_start=r.year("strYearStart"),
548                    year_end=r.year("strYearEnd"), wage=r.s("strWage"), sport=r.s("strSport"))

A player's contract.

Contract( *, raw: Mapping[str, str | None] = <factory>, id: int | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, badge: str | None, year_start: int | None, year_end: int | None, wage: str | None, sport: str | None)
id: int | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
badge: str | None
year_start: int | None
year_end: int | None
wage: str | None
sport: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Country(thesportsdb_client.ApiRecord):
203@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
204class Country(ApiRecord):
205    """A country (``all_countries.php``, ``all/countries``). v1 fills only name and flag32."""
206
207    _summary = ("name", "code")
208    name: str | None  # name_en; use it for country filters
209    name_fr: str | None  # name_fr (v2)
210    code: str | None  # code (v2), e.g. "AD"
211    flag16: str | None  # flag_url_16 (v2)
212    flag32: str | None  # flag_url_32
213    flag64: str | None  # flag_url_64 (v2)
214    api_football_id: int | None  # idAPIfootball (v2)
215
216    @classmethod
217    def _read(cls, r: Rec) -> dict[str, Any]:
218        return dict(name=r.s("name_en"), name_fr=r.s("name_fr"), code=r.s("code"), flag16=r.s("flag_url_16"),
219                    flag32=r.s("flag_url_32"), flag64=r.s("flag_url_64"), api_football_id=r.id_("idAPIfootball"))

A country (all_countries.php, all/countries). v1 fills only name and flag32.

Country( *, raw: Mapping[str, str | None] = <factory>, name: str | None, name_fr: str | None, code: str | None, flag16: str | None, flag32: str | None, flag64: str | None, api_football_id: int | None)
name: str | None
name_fr: str | None
code: str | None
flag16: str | None
flag32: str | None
flag64: str | None
api_football_id: int | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Equipment(thesportsdb_client.ApiRecord):
614@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
615class Equipment(ApiRecord):
616    """A team kit."""
617
618    _summary = ("team_id", "season", "type")
619    id: int | None  # idEquipment
620    team_id: int | None  # idTeam
621    season: str | None  # strSeason
622    type: str | None  # strType, e.g. 1st, 2nd, GK
623    image: str | None  # strEquipment
624    uploaded_by: str | None  # strUsername
625    added: DateTime | None  # date (no zone stated; naive)
626
627    @classmethod
628    def _read(cls, r: Rec) -> dict[str, Any]:
629        return dict(id=r.id_("idEquipment"), team_id=r.id_("idTeam"), season=r.s("strSeason"), type=r.s("strType"),
630                    image=r.s("strEquipment"), uploaded_by=r.s("strUsername"), added=r.ldt("date"))

A team kit.

Equipment( *, raw: Mapping[str, str | None] = <factory>, id: int | None, team_id: int | None, season: str | None, type: str | None, image: str | None, uploaded_by: str | None, added: datetime.datetime | None)
id: int | None
team_id: int | None
season: str | None
type: str | None
image: str | None
uploaded_by: str | None
added: datetime.datetime | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Event(thesportsdb_client.ApiRecord):
693@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
694class Event(ApiRecord):
695    """A game, match, race or other event. Also used for video highlights.
696
697    ``timestamp``, ``date`` and ``time`` are UTC; ``local_date`` and ``local_time`` are the
698    venue's local time and are often missing for future events.
699    """
700
701    _summary = ("id", "name", "timestamp", "status_code")
702    id: int | None  # idEvent
703    name: str | None  # strEvent, e.g. "Arsenal vs Chelsea"
704    alternate_name: str | None  # strEventAlternate, e.g. "Chelsea @ Arsenal"
705    filename: str | None  # strFilename
706    sport: str | None  # strSport
707    league_id: int | None  # idLeague
708    league: str | None  # strLeague
709    league_badge: str | None  # strLeagueBadge
710    season: str | None  # strSeason
711    round: int | None  # intRound: round number, or a stage code (125 quarter-final … 500 pre-season); see stage
712    group: str | None  # strGroup
713    home_team_id: int | None  # idHomeTeam
714    home_team: str | None  # strHomeTeam
715    home_team_badge: str | None  # strHomeTeamBadge
716    away_team_id: int | None  # idAwayTeam
717    away_team: str | None  # strAwayTeam
718    away_team_badge: str | None  # strAwayTeamBadge
719    home_score: int | None  # intHomeScore
720    away_score: int | None  # intAwayScore
721    home_score_extra: int | None  # intHomeScoreExtra
722    away_score_extra: int | None  # intAwayScoreExtra
723    timestamp: DateTime | None  # strTimestamp: start, UTC (aware)
724    date: Date | None  # dateEvent (UTC)
725    time: Time | None  # strTime (UTC)
726    local_date: Date | None  # dateEventLocal
727    local_time: Time | None  # strTimeLocal
728    status_code: str | None  # strStatus: NS, 2H, FT, Q3, PST...; older events often have none
729    is_postponed: bool | None  # strPostponed
730    venue_id: int | None  # idVenue
731    venue: str | None  # strVenue
732    city: str | None  # strCity
733    country: str | None  # strCountry
734    spectators: int | None  # intSpectators
735    official: str | None  # strOfficial
736    result_text: str | None  # strResult
737    description: str | None  # strDescriptionEN
738    thumb: str | None  # strThumb
739    poster: str | None  # strPoster
740    banner: str | None  # strBanner
741    square: str | None  # strSquare
742    fanart: str | None  # strFanart
743    map: str | None  # strMap
744    video: str | None  # strVideo: highlights, usually YouTube
745    tweet: str | None  # strTweet1
746    weather: str | None  # strWeather
747    rating: float | None  # intScore
748    rating_votes: int | None  # intScoreVotes
749    is_locked: bool | None  # strLocked
750    api_football_id: int | None  # idAPIfootball
751
752    @property
753    def status(self) -> EventStatus:
754        return EventStatus.of(self.status_code)
755
756    @property
757    def stage(self) -> RoundStage | None:
758        """The stage when ``round`` is a stage code (e.g. 200 = final), or None for an ordinary round."""
759        return RoundStage.of(self.round)
760
761    @classmethod
762    def _read(cls, r: Rec) -> dict[str, Any]:
763        return dict(
764            id=r.id_("idEvent"), name=r.s("strEvent"), alternate_name=r.s("strEventAlternate"),
765            filename=r.s("strFilename"), sport=r.s("strSport"), league_id=r.id_("idLeague"), league=r.s("strLeague"),
766            league_badge=r.s("strLeagueBadge"), season=r.s("strSeason"), round=r.i("intRound"), group=r.s("strGroup"),
767            home_team_id=r.id_("idHomeTeam"), home_team=r.s("strHomeTeam"), home_team_badge=r.s("strHomeTeamBadge"),
768            away_team_id=r.id_("idAwayTeam"), away_team=r.s("strAwayTeam"), away_team_badge=r.s("strAwayTeamBadge"),
769            home_score=r.i("intHomeScore"), away_score=r.i("intAwayScore"),
770            home_score_extra=r.i("intHomeScoreExtra"), away_score_extra=r.i("intAwayScoreExtra"),
771            timestamp=r.ts("strTimestamp"), date=r.d("dateEvent"), time=r.t("strTime"),
772            local_date=r.d("dateEventLocal"), local_time=r.t("strTimeLocal"), status_code=r.s("strStatus"),
773            is_postponed=r.b("strPostponed"), venue_id=r.id_("idVenue"), venue=r.s("strVenue"), city=r.s("strCity"),
774            country=r.s("strCountry"), spectators=r.i("intSpectators"), official=r.s("strOfficial"),
775            result_text=r.s("strResult"), description=r.s("strDescriptionEN"), thumb=r.s("strThumb"),
776            poster=r.s("strPoster"), banner=r.s("strBanner"), square=r.s("strSquare"), fanart=r.s("strFanart"),
777            map=r.s("strMap"), video=r.s("strVideo"), tweet=r.s("strTweet1"), weather=r.s("strWeather"),
778            rating=r.f("intScore"), rating_votes=r.i("intScoreVotes"), is_locked=r.locked(),
779            api_football_id=r.id_("idAPIfootball"),
780        )

A game, match, race or other event. Also used for video highlights.

timestamp, date and time are UTC; local_date and local_time are the venue's local time and are often missing for future events.

Event( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, alternate_name: str | None, filename: str | None, sport: str | None, league_id: int | None, league: str | None, league_badge: str | None, season: str | None, round: int | None, group: str | None, home_team_id: int | None, home_team: str | None, home_team_badge: str | None, away_team_id: int | None, away_team: str | None, away_team_badge: str | None, home_score: int | None, away_score: int | None, home_score_extra: int | None, away_score_extra: int | None, timestamp: datetime.datetime | None, date: datetime.date | None, time: datetime.time | None, local_date: datetime.date | None, local_time: datetime.time | None, status_code: str | None, is_postponed: bool | None, venue_id: int | None, venue: str | None, city: str | None, country: str | None, spectators: int | None, official: str | None, result_text: str | None, description: str | None, thumb: str | None, poster: str | None, banner: str | None, square: str | None, fanart: str | None, map: str | None, video: str | None, tweet: str | None, weather: str | None, rating: float | None, rating_votes: int | None, is_locked: bool | None, api_football_id: int | None)
id: int | None
name: str | None
alternate_name: str | None
filename: str | None
sport: str | None
league_id: int | None
league: str | None
league_badge: str | None
season: str | None
round: int | None
group: str | None
home_team_id: int | None
home_team: str | None
home_team_badge: str | None
away_team_id: int | None
away_team: str | None
away_team_badge: str | None
home_score: int | None
away_score: int | None
home_score_extra: int | None
away_score_extra: int | None
timestamp: datetime.datetime | None
date: datetime.date | None
time: datetime.time | None
local_date: datetime.date | None
local_time: datetime.time | None
status_code: str | None
is_postponed: bool | None
venue_id: int | None
venue: str | None
city: str | None
country: str | None
spectators: int | None
official: str | None
result_text: str | None
description: str | None
thumb: str | None
poster: str | None
banner: str | None
square: str | None
fanart: str | None
map: str | None
video: str | None
tweet: str | None
weather: str | None
rating: float | None
rating_votes: int | None
is_locked: bool | None
api_football_id: int | None
status: EventStatus
752    @property
753    def status(self) -> EventStatus:
754        return EventStatus.of(self.status_code)
stage: RoundStage | None
756    @property
757    def stage(self) -> RoundStage | None:
758        """The stage when ``round`` is a stage code (e.g. 200 = final), or None for an ordinary round."""
759        return RoundStage.of(self.round)

The stage when round is a stage code (e.g. 200 = final), or None for an ordinary round.

@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class EventResult(thesportsdb_client.ApiRecord):
551@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
552class EventResult(ApiRecord):
553    """One competitor's result in an individual-sport event (race, golf, fight)."""
554
555    _summary = ("event_id", "player", "position")
556    id: int | None  # idResult
557    event_id: int | None  # idEvent
558    event: str | None  # strEvent
559    date: Date | None  # dateEvent
560    season: str | None  # strSeason
561    sport: str | None  # strSport
562    country: str | None  # strCountry
563    player_id: int | None  # idPlayer
564    player: str | None  # strPlayer
565    team_id: int | None  # idTeam
566    position: int | None  # intPosition
567    points: int | None  # intPoints
568    result: str | None  # strResult
569    detail: str | None  # strDetail, e.g. a time gap "+29.520"
570
571    @classmethod
572    def _read(cls, r: Rec) -> dict[str, Any]:
573        return dict(id=r.id_("idResult"), event_id=r.id_("idEvent"), event=r.s("strEvent"), date=r.d("dateEvent"),
574                    season=r.s("strSeason"), sport=r.s("strSport"), country=r.s("strCountry"),
575                    player_id=r.id_("idPlayer"), player=r.s("strPlayer"), team_id=r.id_("idTeam"),
576                    position=r.i("intPosition"), points=r.i("intPoints"), result=r.s("strResult"),
577                    detail=r.s("strDetail"))

One competitor's result in an individual-sport event (race, golf, fight).

EventResult( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, event: str | None, date: datetime.date | None, season: str | None, sport: str | None, country: str | None, player_id: int | None, player: str | None, team_id: int | None, position: int | None, points: int | None, result: str | None, detail: str | None)
id: int | None
event_id: int | None
event: str | None
date: datetime.date | None
season: str | None
sport: str | None
country: str | None
player_id: int | None
player: str | None
team_id: int | None
position: int | None
points: int | None
result: str | None
detail: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class EventStat(thesportsdb_client.ApiRecord):
891@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
892class EventStat(ApiRecord):
893    """One team statistic for an event, home vs away."""
894
895    _summary = ("event_id", "name", "home", "away")
896    id: int | None  # idStatistic
897    event_id: int | None  # idEvent
898    event: str | None  # strEvent
899    name: str | None  # strStat
900    home: float | None  # intHome
901    away: float | None  # intAway
902    api_football_id: int | None  # idApiFootball (note the capitalisation)
903
904    @classmethod
905    def _read(cls, r: Rec) -> dict[str, Any]:
906        return dict(id=r.id_("idStatistic"), event_id=r.id_("idEvent"), event=r.s("strEvent"), name=r.s("strStat"),
907                    home=r.f("intHome"), away=r.f("intAway"), api_football_id=r.id_("idApiFootball"))

One team statistic for an event, home vs away.

EventStat( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, event: str | None, name: str | None, home: float | None, away: float | None, api_football_id: int | None)
id: int | None
event_id: int | None
event: str | None
name: str | None
home: float | None
away: float | None
api_football_id: int | None
class EventStatus(enum.Enum):
117class EventStatus(Enum):
118    """A broad reading of a ``strStatus`` code, so you don't need every sport's codes.
119
120    Covers the codes in TheSportsDB's data documentation (docs_api_data) for every sport.
121    """
122
123    NOT_STARTED = "not_started"
124    IN_PLAY = "in_play"
125    FINISHED = "finished"
126    POSTPONED = "postponed"
127    INTERRUPTED = "interrupted"  # suspended or interrupted (SUSP, INT, INTR): stopped, and may resume
128    CANCELLED = "cancelled"
129    ABANDONED = "abandoned"
130    UNKNOWN = "unknown"  # None, or a code this library doesn't know
131
132    @staticmethod
133    def of(code: str | None) -> EventStatus:
134        """Classifies a raw code (``NS``, ``2H``, ``Q3``, ``P2``, ``IN4``, ``S2``, ``FT``, ``PST``...)."""
135        c = (code or "").strip().upper()
136        if not c:
137            return EventStatus.UNKNOWN
138        for status, codes in _STATUS_CODES:
139            if c in codes:
140                return status
141        if re.fullmatch(r"IN\d+|S\d", c):  # baseball innings, volleyball sets
142            return EventStatus.IN_PLAY
143        return EventStatus.UNKNOWN

A broad reading of a strStatus code, so you don't need every sport's codes.

Covers the codes in TheSportsDB's data documentation (docs_api_data) for every sport.

NOT_STARTED = <EventStatus.NOT_STARTED: 'not_started'>
IN_PLAY = <EventStatus.IN_PLAY: 'in_play'>
FINISHED = <EventStatus.FINISHED: 'finished'>
POSTPONED = <EventStatus.POSTPONED: 'postponed'>
INTERRUPTED = <EventStatus.INTERRUPTED: 'interrupted'>
CANCELLED = <EventStatus.CANCELLED: 'cancelled'>
ABANDONED = <EventStatus.ABANDONED: 'abandoned'>
UNKNOWN = <EventStatus.UNKNOWN: 'unknown'>
@staticmethod
def of(code: str | None) -> EventStatus:
132    @staticmethod
133    def of(code: str | None) -> EventStatus:
134        """Classifies a raw code (``NS``, ``2H``, ``Q3``, ``P2``, ``IN4``, ``S2``, ``FT``, ``PST``...)."""
135        c = (code or "").strip().upper()
136        if not c:
137            return EventStatus.UNKNOWN
138        for status, codes in _STATUS_CODES:
139            if c in codes:
140                return status
141        if re.fullmatch(r"IN\d+|S\d", c):  # baseball innings, volleyball sets
142            return EventStatus.IN_PLAY
143        return EventStatus.UNKNOWN

Classifies a raw code (NS, 2H, Q3, P2, IN4, S2, FT, PST...).

@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class FormerTeam(thesportsdb_client.ApiRecord):
478@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
479class FormerTeam(ApiRecord):
480    """A team a player used to play for (``lookupformerteams.php``, ``lookup/player_teams``)."""
481
482    _summary = ("player", "team", "joined", "departed")
483    id: int | None  # id
484    player_id: int | None  # idPlayer
485    player: str | None  # strPlayer
486    team_id: int | None  # idFormerTeam
487    team: str | None  # strFormerTeam
488    badge: str | None  # strBadge
489    joined: str | None  # strJoined, usually a year
490    departed: str | None  # strDeparted, usually a year
491    move_type: str | None  # strMoveType, e.g. Permanent, Loan
492    appearances: int | None  # intAppearances
493    goals: int | None  # intGoals
494    sport: str | None  # strSport
495
496    @classmethod
497    def _read(cls, r: Rec) -> dict[str, Any]:
498        return dict(id=r.id_("id"), player_id=r.id_("idPlayer"), player=r.s("strPlayer"),
499                    team_id=r.id_("idFormerTeam"), team=r.s("strFormerTeam"), badge=r.s("strBadge"),
500                    joined=r.s("strJoined"), departed=r.s("strDeparted"), move_type=r.s("strMoveType"),
501                    appearances=r.i("intAppearances"), goals=r.i("intGoals"), sport=r.s("strSport"))

A team a player used to play for (lookupformerteams.php, lookup/player_teams).

FormerTeam( *, raw: Mapping[str, str | None] = <factory>, id: int | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, badge: str | None, joined: str | None, departed: str | None, move_type: str | None, appearances: int | None, goals: int | None, sport: str | None)
id: int | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
badge: str | None
joined: str | None
departed: str | None
move_type: str | None
appearances: int | None
goals: int | None
sport: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Honour(thesportsdb_client.ApiRecord):
451@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
452class Honour(ApiRecord):
453    """A trophy a player won (``lookuphonours.php``, ``lookup/player_honours``)."""
454
455    _summary = ("player", "name", "season")
456    id: int | None  # id
457    honour_id: int | None  # idHonour
458    name: str | None  # strHonour
459    season: str | None  # strSeason
460    player_id: int | None  # idPlayer
461    player: str | None  # strPlayer
462    team_id: int | None  # idTeam
463    team: str | None  # strTeam
464    team_badge: str | None  # strTeamBadge
465    league_id: int | None  # idLeague
466    sport: str | None  # strSport
467    logo: str | None  # strHonourLogo
468    trophy: str | None  # strHonourTrophy
469
470    @classmethod
471    def _read(cls, r: Rec) -> dict[str, Any]:
472        return dict(id=r.id_("id"), honour_id=r.id_("idHonour"), name=r.s("strHonour"), season=r.s("strSeason"),
473                    player_id=r.id_("idPlayer"), player=r.s("strPlayer"), team_id=r.id_("idTeam"),
474                    team=r.s("strTeam"), team_badge=r.s("strTeamBadge"), league_id=r.id_("idLeague"),
475                    sport=r.s("strSport"), logo=r.s("strHonourLogo"), trophy=r.s("strHonourTrophy"))

A trophy a player won (lookuphonours.php, lookup/player_honours).

Honour( *, raw: Mapping[str, str | None] = <factory>, id: int | None, honour_id: int | None, name: str | None, season: str | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, team_badge: str | None, league_id: int | None, sport: str | None, logo: str | None, trophy: str | None)
id: int | None
honour_id: int | None
name: str | None
season: str | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
team_badge: str | None
league_id: int | None
sport: str | None
trophy: str | None
class ImageSize(enum.Enum):
 96class ImageSize(Enum):
 97    """Sizes TheSportsDB serves by appending a path suffix to an image URL."""
 98
 99    MEDIUM = "/medium"  # about 70% of the bytes
100    SMALL = "/small"  # about 35%; good for cards
101    TINY = "/tiny"  # about 10%; good for lists

Sizes TheSportsDB serves by appending a path suffix to an image URL.

MEDIUM = <ImageSize.MEDIUM: '/medium'>
SMALL = <ImageSize.SMALL: '/small'>
TINY = <ImageSize.TINY: '/tiny'>
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class League(thesportsdb_client.ApiRecord):
222@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
223class League(ApiRecord):
224    """A league or cup. Lists fill only a few fields; look it up by id for the rest."""
225
226    _summary = ("id", "name")
227    id: int | None  # idLeague
228    name: str | None  # strLeague
229    alternate_names: tuple[str, ...]  # strLeagueAlternate, split on commas (premium only in all_leagues.php)
230    sport: str | None  # strSport
231    country: str | None  # strCountry
232    gender: str | None  # strGender
233    current_season: str | None  # strCurrentSeason, e.g. "2026-2027" or "2026"
234    division: int | None  # intDivision
235    is_cup: bool | None  # idCup
236    formed_year: int | None  # intFormedYear
237    first_event_date: Date | None  # dateFirstEvent
238    is_complete: bool | None  # strComplete
239    event_naming: str | None  # strNaming, e.g. "{strHomeTeam} vs {strAwayTeam}"
240    tv_rights: str | None  # strTvRights
241    badge: str | None  # strBadge
242    logo: str | None  # strLogo
243    banner: str | None  # strBanner
244    poster: str | None  # strPoster
245    trophy: str | None  # strTrophy
246    fanart: tuple[str, ...]  # strFanart1..4
247    descriptions: Mapping[str, str]  # strDescriptionEN, DE, ... keyed by language code
248    socials: Socials
249    is_locked: bool | None  # strLocked
250    api_football_id: int | None  # idAPIfootball
251    api_football_v3_id: int | None  # idAPIfootballv3
252
253    @property
254    def description(self) -> str | None:
255        """The English description."""
256        return self.descriptions.get("EN")
257
258    @classmethod
259    def _read(cls, r: Rec) -> dict[str, Any]:
260        return dict(
261            id=r.id_("idLeague"), name=r.s("strLeague"), alternate_names=r.csv("strLeagueAlternate"),
262            sport=r.s("strSport"), country=r.s("strCountry"), gender=r.s("strGender"),
263            current_season=r.s("strCurrentSeason"), division=r.i("intDivision"), is_cup=r.b("idCup"),
264            formed_year=r.year("intFormedYear"), first_event_date=r.d("dateFirstEvent"),
265            is_complete=r.b("strComplete"), event_naming=r.s("strNaming"), tv_rights=r.s("strTvRights"),
266            badge=r.s("strBadge"), logo=r.s("strLogo"), banner=r.s("strBanner"), poster=r.s("strPoster"),
267            trophy=r.s("strTrophy"), fanart=r.numbered("strFanart", 4),
268            descriptions=MappingProxyType(r.descriptions()), socials=Socials._read(r), is_locked=r.locked(),
269            api_football_id=r.id_("idAPIfootball"), api_football_v3_id=r.id_("idAPIfootballv3"),
270        )

A league or cup. Lists fill only a few fields; look it up by id for the rest.

League( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, alternate_names: tuple[str, ...], sport: str | None, country: str | None, gender: str | None, current_season: str | None, division: int | None, is_cup: bool | None, formed_year: int | None, first_event_date: datetime.date | None, is_complete: bool | None, event_naming: str | None, tv_rights: str | None, badge: str | None, logo: str | None, banner: str | None, poster: str | None, trophy: str | None, fanart: tuple[str, ...], descriptions: Mapping[str, str], socials: Socials, is_locked: bool | None, api_football_id: int | None, api_football_v3_id: int | None)
id: int | None
name: str | None
alternate_names: tuple[str, ...]
sport: str | None
country: str | None
gender: str | None
current_season: str | None
division: int | None
is_cup: bool | None
formed_year: int | None
first_event_date: datetime.date | None
is_complete: bool | None
event_naming: str | None
tv_rights: str | None
badge: str | None
banner: str | None
poster: str | None
trophy: str | None
fanart: tuple[str, ...]
descriptions: Mapping[str, str]
socials: Socials
is_locked: bool | None
api_football_id: int | None
api_football_v3_id: int | None
description: str | None
253    @property
254    def description(self) -> str | None:
255        """The English description."""
256        return self.descriptions.get("EN")

The English description.

@dataclass(frozen=True)
class LeagueRef:
76@dataclass(frozen=True)
77class LeagueRef:
78    """A league a team plays in: one of ``idLeague``/``strLeague`` .. ``idLeague7``/``strLeague7``."""
79
80    id: int
81    name: str | None

A league a team plays in: one of idLeague/strLeague .. idLeague7/strLeague7.

LeagueRef(id: int, name: str | None)
id: int
name: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class LineupEntry(thesportsdb_client.ApiRecord):
820@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
821class LineupEntry(ApiRecord):
822    """One player in an event's lineup."""
823
824    _summary = ("event_id", "player", "team")
825    id: int | None  # idLineup
826    event_id: int | None  # idEvent
827    event: str | None  # strEvent (v2)
828    player_id: int | None  # idPlayer
829    player: str | None  # strPlayer
830    team_id: int | None  # idTeam
831    team: str | None  # strTeam
832    is_home: bool | None  # strHome
833    is_substitute: bool | None  # strSubstitute
834    position: str | None  # strPosition
835    position_short: str | None  # strPositionShort (v2; often null)
836    formation: str | None  # strFormation (v2; often null)
837    squad_number: int | None  # intSquadNumber
838    country: str | None  # strCountry (v2)
839    season: str | None  # strSeason (v2)
840    thumb: str | None  # strThumb (v1)
841    cutout: str | None  # strCutout
842    render: str | None  # strRender (v1)
843    api_football_id: int | None  # idAPIfootball (v2)
844
845    @classmethod
846    def _read(cls, r: Rec) -> dict[str, Any]:
847        return dict(id=r.id_("idLineup"), event_id=r.id_("idEvent"), event=r.s("strEvent"),
848                    player_id=r.id_("idPlayer"), player=r.s("strPlayer"), team_id=r.id_("idTeam"),
849                    team=r.s("strTeam"), is_home=r.b("strHome"), is_substitute=r.b("strSubstitute"),
850                    position=r.s("strPosition"), position_short=r.s("strPositionShort"),
851                    formation=r.s("strFormation"), squad_number=r.i("intSquadNumber"), country=r.s("strCountry"),
852                    season=r.s("strSeason"), thumb=r.s("strThumb"), cutout=r.s("strCutout"),
853                    render=r.s("strRender"), api_football_id=r.id_("idAPIfootball"))

One player in an event's lineup.

LineupEntry( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, event: str | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, is_home: bool | None, is_substitute: bool | None, position: str | None, position_short: str | None, formation: str | None, squad_number: int | None, country: str | None, season: str | None, thumb: str | None, cutout: str | None, render: str | None, api_football_id: int | None)
id: int | None
event_id: int | None
event: str | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
is_home: bool | None
is_substitute: bool | None
position: str | None
position_short: str | None
formation: str | None
squad_number: int | None
country: str | None
season: str | None
thumb: str | None
cutout: str | None
render: str | None
api_football_id: int | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class LiveScore(thesportsdb_client.ApiRecord):
945@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
946class LiveScore(ApiRecord):
947    """A game in progress. Entries can be stale: check ``updated``."""
948
949    _summary = ("event_id", "home_team", "home_score", "away_score", "away_team", "status_code")
950    id: int | None  # idLiveScore
951    event_id: int | None  # idEvent
952    sport: str | None  # strSport
953    league_id: int | None  # idLeague
954    league: str | None  # strLeague
955    division: int | None  # intDivision
956    home_team_id: int | None  # idHomeTeam
957    home_team: str | None  # strHomeTeam
958    home_team_badge: str | None  # strHomeTeamBadge
959    away_team_id: int | None  # idAwayTeam
960    away_team: str | None  # strAwayTeam
961    away_team_badge: str | None  # strAwayTeamBadge
962    home_score: int | None  # intHomeScore
963    away_score: int | None  # intAwayScore
964    status_code: str | None  # strStatus
965    progress: str | None  # strProgress: the minute, or "Final"
966    event_time: Time | None  # strEventTime, HH:mm
967    date: Date | None  # dateEvent
968    timestamp: DateTime | None  # strTimestamp: kick-off, UTC
969    updated: DateTime | None  # updated (naive; zone not stated)
970
971    @property
972    def status(self) -> EventStatus:
973        return EventStatus.of(self.status_code)
974
975    @classmethod
976    def _read(cls, r: Rec) -> dict[str, Any]:
977        return dict(id=r.id_("idLiveScore"), event_id=r.id_("idEvent"), sport=r.s("strSport"),
978                    league_id=r.id_("idLeague"), league=r.s("strLeague"), division=r.i("intDivision"),
979                    home_team_id=r.id_("idHomeTeam"), home_team=r.s("strHomeTeam"),
980                    home_team_badge=r.s("strHomeTeamBadge"), away_team_id=r.id_("idAwayTeam"),
981                    away_team=r.s("strAwayTeam"), away_team_badge=r.s("strAwayTeamBadge"),
982                    home_score=r.i("intHomeScore"), away_score=r.i("intAwayScore"), status_code=r.s("strStatus"),
983                    progress=r.s("strProgress"), event_time=r.t("strEventTime"), date=r.d("dateEvent"),
984                    timestamp=r.ts("strTimestamp"), updated=r.ldt("updated"))

A game in progress. Entries can be stale: check updated.

LiveScore( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, sport: str | None, league_id: int | None, league: str | None, division: int | None, home_team_id: int | None, home_team: str | None, home_team_badge: str | None, away_team_id: int | None, away_team: str | None, away_team_badge: str | None, home_score: int | None, away_score: int | None, status_code: str | None, progress: str | None, event_time: datetime.time | None, date: datetime.date | None, timestamp: datetime.datetime | None, updated: datetime.datetime | None)
id: int | None
event_id: int | None
sport: str | None
league_id: int | None
league: str | None
division: int | None
home_team_id: int | None
home_team: str | None
home_team_badge: str | None
away_team_id: int | None
away_team: str | None
away_team_badge: str | None
home_score: int | None
away_score: int | None
status_code: str | None
progress: str | None
event_time: datetime.time | None
date: datetime.date | None
timestamp: datetime.datetime | None
updated: datetime.datetime | None
status: EventStatus
971    @property
972    def status(self) -> EventStatus:
973        return EventStatus.of(self.status_code)
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Milestone(thesportsdb_client.ApiRecord):
504@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
505class Milestone(ApiRecord):
506    """A career milestone or award."""
507
508    _summary = ("player", "name", "date")
509    id: int | None  # id
510    milestone_id: int | None  # idMilestone
511    name: str | None  # strMilestone
512    date: Date | None  # dateMilestone
513    player_id: int | None  # idPlayer
514    player: str | None  # strPlayer
515    team_id: int | None  # idTeam
516    team: str | None  # strTeam
517    sport: str | None  # strSport
518    logo: str | None  # strMilestoneLogo
519
520    @classmethod
521    def _read(cls, r: Rec) -> dict[str, Any]:
522        return dict(id=r.id_("id"), milestone_id=r.id_("idMilestone"), name=r.s("strMilestone"),
523                    date=r.d("dateMilestone"), player_id=r.id_("idPlayer"), player=r.s("strPlayer"),
524                    team_id=r.id_("idTeam"), team=r.s("strTeam"), sport=r.s("strSport"),
525                    logo=r.s("strMilestoneLogo"))

A career milestone or award.

Milestone( *, raw: Mapping[str, str | None] = <factory>, id: int | None, milestone_id: int | None, name: str | None, date: datetime.date | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, sport: str | None, logo: str | None)
id: int | None
milestone_id: int | None
name: str | None
date: datetime.date | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
sport: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Player(thesportsdb_client.ApiRecord):
371@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
372class Player(ApiRecord):
373    """A player, manager or other person. Search and list endpoints fill only some fields."""
374
375    _summary = ("id", "name", "team")
376    id: int | None  # idPlayer
377    name: str | None  # strPlayer
378    alternate_name: str | None  # strPlayerAlternate
379    last_name: str | None  # strLastName
380    team_id: int | None  # idTeam
381    team: str | None  # strTeam
382    second_team_id: int | None  # idTeam2
383    second_team: str | None  # strTeam2, often the national team
384    national_team_id: int | None  # idTeamNational
385    manager_id: int | None  # idPlayerManager
386    sport: str | None  # strSport
387    nationality: str | None  # strNationality
388    position: str | None  # strPosition
389    number: str | None  # strNumber, as text
390    status: str | None  # strStatus, e.g. Active, Retired
391    gender: str | None  # strGender
392    born: Date | None  # dateBorn
393    birth_location: str | None  # strBirthLocation
394    died: Date | None  # dateDied
395    death_location: str | None  # strDeathLocation
396    signed: Date | None  # dateSigned
397    signing: str | None  # strSigning: the fee as text
398    wage: str | None  # strWage, as text
399    agent: str | None  # strAgent
400    height: str | None  # strHeight, as text
401    weight: str | None  # strWeight, as text
402    side: str | None  # strSide: Left/Right
403    kit: str | None  # strKit
404    outfitter: str | None  # strOutfitter
405    college: str | None  # strCollege
406    ethnicity: str | None  # strEthnicity
407    thumb: str | None  # strThumb
408    cutout: str | None  # strCutout
409    render: str | None  # strRender
410    cartoon: str | None  # strCartoon
411    banner: str | None  # strBanner
412    poster: str | None  # strPoster
413    fanart: tuple[str, ...]  # strFanart1..4
414    creative_commons: bool | None
415    """strCreativeCommons. TheSportsDB's terms: artwork that isn't Creative Commons must not be used in published apps."""
416    creative_commons_attribution: str | None  # strCreativeCommonsAttribution: the credit to show
417    descriptions: Mapping[str, str]
418    socials: Socials
419    loved: int | None  # intLoved
420    is_locked: bool | None  # strLocked
421    relevance: float | None  # relevance: search score (v1 searchplayers.php only)
422    external_ids: PlayerExternalIds
423
424    @property
425    def description(self) -> str | None:
426        return self.descriptions.get("EN")
427
428    @classmethod
429    def _read(cls, r: Rec) -> dict[str, Any]:
430        return dict(
431            id=r.id_("idPlayer"), name=r.s("strPlayer"), alternate_name=r.s("strPlayerAlternate"),
432            last_name=r.s("strLastName"), team_id=r.id_("idTeam"), team=r.s("strTeam"),
433            second_team_id=r.id_("idTeam2"), second_team=r.s("strTeam2"), national_team_id=r.id_("idTeamNational"),
434            manager_id=r.id_("idPlayerManager"), sport=r.s("strSport"), nationality=r.s("strNationality"),
435            position=r.s("strPosition"), number=r.s("strNumber"), status=r.s("strStatus"), gender=r.s("strGender"),
436            born=r.d("dateBorn"), birth_location=r.s("strBirthLocation"), died=r.d("dateDied"),
437            death_location=r.s("strDeathLocation"), signed=r.d("dateSigned"), signing=r.s("strSigning"),
438            wage=r.s("strWage"), agent=r.s("strAgent"), height=r.s("strHeight"), weight=r.s("strWeight"),
439            side=r.s("strSide"), kit=r.s("strKit"), outfitter=r.s("strOutfitter"), college=r.s("strCollege"),
440            ethnicity=r.s("strEthnicity"), thumb=r.s("strThumb"), cutout=r.s("strCutout"), render=r.s("strRender"),
441            cartoon=r.s("strCartoon"), banner=r.s("strBanner"), poster=r.s("strPoster"),
442            fanart=r.numbered("strFanart", 4), creative_commons=r.b("strCreativeCommons"),
443            creative_commons_attribution=r.s("strCreativeCommonsAttribution"),
444            descriptions=MappingProxyType(r.descriptions()), socials=Socials._read(r), loved=r.i("intLoved"),
445            is_locked=r.locked(), relevance=r.f("relevance"),
446            external_ids=PlayerExternalIds(r.id_("idAPIfootball"), r.s("idESPN"), r.s("idGoogle"),
447                                           r.s("idTransferMkt"), r.s("idWikidata"), r.s("intSoccerXMLTeamID")),
448        )

A player, manager or other person. Search and list endpoints fill only some fields.

Player( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, alternate_name: str | None, last_name: str | None, team_id: int | None, team: str | None, second_team_id: int | None, second_team: str | None, national_team_id: int | None, manager_id: int | None, sport: str | None, nationality: str | None, position: str | None, number: str | None, status: str | None, gender: str | None, born: datetime.date | None, birth_location: str | None, died: datetime.date | None, death_location: str | None, signed: datetime.date | None, signing: str | None, wage: str | None, agent: str | None, height: str | None, weight: str | None, side: str | None, kit: str | None, outfitter: str | None, college: str | None, ethnicity: str | None, thumb: str | None, cutout: str | None, render: str | None, cartoon: str | None, banner: str | None, poster: str | None, fanart: tuple[str, ...], creative_commons: bool | None, creative_commons_attribution: str | None, descriptions: Mapping[str, str], socials: Socials, loved: int | None, is_locked: bool | None, relevance: float | None, external_ids: PlayerExternalIds)
id: int | None
name: str | None
alternate_name: str | None
last_name: str | None
team_id: int | None
team: str | None
second_team_id: int | None
second_team: str | None
national_team_id: int | None
manager_id: int | None
sport: str | None
nationality: str | None
position: str | None
number: str | None
status: str | None
gender: str | None
born: datetime.date | None
birth_location: str | None
died: datetime.date | None
death_location: str | None
signed: datetime.date | None
signing: str | None
wage: str | None
agent: str | None
height: str | None
weight: str | None
side: str | None
kit: str | None
outfitter: str | None
college: str | None
ethnicity: str | None
thumb: str | None
cutout: str | None
render: str | None
cartoon: str | None
banner: str | None
poster: str | None
fanart: tuple[str, ...]
creative_commons: bool | None

strCreativeCommons. TheSportsDB's terms: artwork that isn't Creative Commons must not be used in published apps.

creative_commons_attribution: str | None
descriptions: Mapping[str, str]
socials: Socials
loved: int | None
is_locked: bool | None
relevance: float | None
external_ids: PlayerExternalIds
description: str | None
424    @property
425    def description(self) -> str | None:
426        return self.descriptions.get("EN")
@dataclass(frozen=True)
class PlayerExternalIds:
84@dataclass(frozen=True)
85class PlayerExternalIds:
86    """Ids of the same player in other databases."""
87
88    api_football: int | None = None  # idAPIfootball
89    espn: str | None = None  # idESPN
90    google: str | None = None  # idGoogle, e.g. /g/11cpprmr81
91    transfermarkt: str | None = None  # idTransferMkt
92    wikidata: str | None = None  # idWikidata, e.g. Q9144353
93    soccer_xml_team: str | None = None  # intSoccerXMLTeamID

Ids of the same player in other databases.

PlayerExternalIds( api_football: int | None = None, espn: str | None = None, google: str | None = None, transfermarkt: str | None = None, wikidata: str | None = None, soccer_xml_team: str | None = None)
api_football: int | None = None
espn: str | None = None
google: str | None = None
transfermarkt: str | None = None
wikidata: str | None = None
soccer_xml_team: str | None = None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class PlayerStat(thesportsdb_client.ApiRecord):
580@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
581class PlayerStat(ApiRecord):
582    """One statistic for a player in one season."""
583
584    _summary = ("player", "season", "statistic", "value")
585    id: int | None  # id
586    player_id: int | None  # idPlayer
587    player: str | None  # strPlayer
588    team_id: int | None  # idTeam
589    team: str | None  # strTeam
590    team_badge: str | None  # strTeamBadge
591    league_id: int | None  # idLeague
592    league: str | None  # strLeague
593    league_badge: str | None  # strLeagueBadge
594    season: str | None  # strSeason
595    sport: str | None  # strSport
596    statistic: str | None  # strStatistic, e.g. Goals
597    value: str | None  # strValue, as text
598
599    @property
600    def numeric_value(self) -> float | None:
601        try:
602            return float(self.value) if self.value is not None else None
603        except ValueError:
604            return None
605
606    @classmethod
607    def _read(cls, r: Rec) -> dict[str, Any]:
608        return dict(id=r.id_("id"), player_id=r.id_("idPlayer"), player=r.s("strPlayer"), team_id=r.id_("idTeam"),
609                    team=r.s("strTeam"), team_badge=r.s("strTeamBadge"), league_id=r.id_("idLeague"),
610                    league=r.s("strLeague"), league_badge=r.s("strLeagueBadge"), season=r.s("strSeason"),
611                    sport=r.s("strSport"), statistic=r.s("strStatistic"), value=r.s("strValue"))

One statistic for a player in one season.

PlayerStat( *, raw: Mapping[str, str | None] = <factory>, id: int | None, player_id: int | None, player: str | None, team_id: int | None, team: str | None, team_badge: str | None, league_id: int | None, league: str | None, league_badge: str | None, season: str | None, sport: str | None, statistic: str | None, value: str | None)
id: int | None
player_id: int | None
player: str | None
team_id: int | None
team: str | None
team_badge: str | None
league_id: int | None
league: str | None
league_badge: str | None
season: str | None
sport: str | None
statistic: str | None
value: str | None
numeric_value: float | None
599    @property
600    def numeric_value(self) -> float | None:
601        try:
602            return float(self.value) if self.value is not None else None
603        except ValueError:
604            return None
class RoundStage(enum.Enum):
158class RoundStage(Enum):
159    """The stage a special ``intRound`` value stands for (docs_api_data). Other values are ordinary rounds."""
160
161    QUARTER_FINAL = 125
162    SEMI_FINAL = 150
163    PLAYOFF = 160
164    PLAYOFF_SEMI_FINAL = 170
165    PLAYOFF_FINAL = 180
166    FINAL = 200
167    QUALIFIER = 400
168    PRE_SEASON = 500
169
170    @staticmethod
171    def of(round: int | None) -> RoundStage | None:
172        """The stage for an ``intRound`` value, or None for an ordinary round number."""
173        try:
174            return RoundStage(round) if round is not None else None
175        except ValueError:
176            return None

The stage a special intRound value stands for (docs_api_data). Other values are ordinary rounds.

QUARTER_FINAL = <RoundStage.QUARTER_FINAL: 125>
SEMI_FINAL = <RoundStage.SEMI_FINAL: 150>
PLAYOFF = <RoundStage.PLAYOFF: 160>
PLAYOFF_SEMI_FINAL = <RoundStage.PLAYOFF_SEMI_FINAL: 170>
PLAYOFF_FINAL = <RoundStage.PLAYOFF_FINAL: 180>
FINAL = <RoundStage.FINAL: 200>
QUALIFIER = <RoundStage.QUALIFIER: 400>
PRE_SEASON = <RoundStage.PRE_SEASON: 500>
@staticmethod
def of(round: int | None) -> RoundStage | None:
170    @staticmethod
171    def of(round: int | None) -> RoundStage | None:
172        """The stage for an ``intRound`` value, or None for an ordinary round number."""
173        try:
174            return RoundStage(round) if round is not None else None
175        except ValueError:
176            return None

The stage for an intRound value, or None for an ordinary round number.

@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Season(thesportsdb_client.ApiRecord):
273@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
274class Season(ApiRecord):
275    """A season of a league (``search_all_seasons.php``, ``list/seasons``)."""
276
277    _summary = ("name",)
278    name: str  # strSeason, e.g. "2026-2027"
279    badge: str | None  # strBadge (v2, or v1 with badges=True)
280    poster: str | None  # strPoster (v2, or v1 with posters=True)
281    description: str | None  # strDescriptionEN (v2, or v1 with descriptions=True)
282
283    @classmethod
284    def _read(cls, r: Rec) -> dict[str, Any]:
285        return dict(name=r.s("strSeason") or "", badge=r.s("strBadge"), poster=r.s("strPoster"),
286                    description=r.s("strDescriptionEN"))

A season of a league (search_all_seasons.php, list/seasons).

Season( *, raw: Mapping[str, str | None] = <factory>, name: str, badge: str | None, poster: str | None, description: str | None)
name: str
badge: str | None
poster: str | None
description: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class SeasonPoster(thesportsdb_client.ApiRecord):
289@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
290class SeasonPoster(ApiRecord):
291    """One season poster or badge uploaded for a league (v2 ``list/seasonposters``); several per season are possible."""
292
293    _summary = ("id", "season")
294    id: int | None  # idArt: this artwork's id
295    league_id: int | None  # idLeague
296    season: str | None  # strSeason
297    poster: str | None  # strPoster
298    badge: str | None  # strBadge
299    description: str | None  # strDescriptionEN
300    uploaded_by: str | None  # strUsername
301
302    @classmethod
303    def _read(cls, r: Rec) -> dict[str, Any]:
304        return dict(id=r.id_("idArt"), league_id=r.id_("idLeague"), season=r.s("strSeason"), poster=r.s("strPoster"),
305                    badge=r.s("strBadge"), description=r.s("strDescriptionEN"), uploaded_by=r.s("strUsername"))

One season poster or badge uploaded for a league (v2 list/seasonposters); several per season are possible.

SeasonPoster( *, raw: Mapping[str, str | None] = <factory>, id: int | None, league_id: int | None, season: str | None, poster: str | None, badge: str | None, description: str | None, uploaded_by: str | None)
id: int | None
league_id: int | None
season: str | None
poster: str | None
badge: str | None
description: str | None
uploaded_by: str | None
@dataclass(frozen=True)
class Socials:
59@dataclass(frozen=True)
60class Socials:
61    """Web and social links. Often without a scheme (``www.facebook.com/Arsenal``)."""
62
63    website: str | None = None  # strWebsite
64    facebook: str | None = None  # strFacebook
65    twitter: str | None = None  # strTwitter
66    instagram: str | None = None  # strInstagram
67    youtube: str | None = None  # strYoutube
68    rss: str | None = None  # strRSS
69
70    @staticmethod
71    def _read(r: Rec) -> Socials:
72        return Socials(r.s("strWebsite"), r.s("strFacebook"), r.s("strTwitter"), r.s("strInstagram"),
73                       r.s("strYoutube"), r.s("strRSS"))

Web and social links. Often without a scheme (www.facebook.com/Arsenal).

Socials( website: str | None = None, facebook: str | None = None, twitter: str | None = None, instagram: str | None = None, youtube: str | None = None, rss: str | None = None)
website: str | None = None
facebook: str | None = None
twitter: str | None = None
instagram: str | None = None
youtube: str | None = None
rss: str | None = None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Sport(thesportsdb_client.ApiRecord):
183@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
184class Sport(ApiRecord):
185    """A sport (``all_sports.php``, ``all/sports``)."""
186
187    _summary = ("id", "name")
188    id: int | None  # idSport
189    name: str | None  # strSport, e.g. "Ice Hockey"; use it for sport filters
190    format: str | None  # strFormat: TeamvsTeam or EventSport
191    description: str | None  # strSportDescription
192    thumb: str | None  # strSportThumb
193    thumb_black_and_white: str | None  # strSportThumbBW
194    icon: str | None  # strSportIconGreen
195
196    @classmethod
197    def _read(cls, r: Rec) -> dict[str, Any]:
198        return dict(id=r.id_("idSport"), name=r.s("strSport"), format=r.s("strFormat"),
199                    description=r.s("strSportDescription"), thumb=r.s("strSportThumb"),
200                    thumb_black_and_white=r.s("strSportThumbBW"), icon=r.s("strSportIconGreen"))

A sport (all_sports.php, all/sports).

Sport( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, format: str | None, description: str | None, thumb: str | None, thumb_black_and_white: str | None, icon: str | None)
id: int | None
name: str | None
format: str | None
description: str | None
thumb: str | None
thumb_black_and_white: str | None
icon: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Standing(thesportsdb_client.ApiRecord):
783@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
784class Standing(ApiRecord):
785    """One row of a league table (``lookuptable.php``; v1 only)."""
786
787    _summary = ("rank", "team", "points")
788    id: int | None  # idStanding
789    rank: int | None  # intRank
790    team_id: int | None  # idTeam
791    team: str | None  # strTeam
792    badge: str | None  # strBadge
793    league_id: int | None  # idLeague
794    league: str | None  # strLeague
795    season: str | None  # strSeason
796    group: str | None  # strGroup
797    form: str | None  # strForm, e.g. "WWDLW"
798    description: str | None  # strDescription, e.g. "Promotion - Champions League"
799    played: int | None  # intPlayed
800    won: int | None  # intWin
801    drawn: int | None  # intDraw
802    lost: int | None  # intLoss
803    goals_for: int | None  # intGoalsFor
804    goals_against: int | None  # intGoalsAgainst
805    goal_difference: int | None  # intGoalDifference
806    points: int | None  # intPoints
807    updated: DateTime | None  # dateUpdated (naive)
808
809    @classmethod
810    def _read(cls, r: Rec) -> dict[str, Any]:
811        return dict(id=r.id_("idStanding"), rank=r.i("intRank"), team_id=r.id_("idTeam"), team=r.s("strTeam"),
812                    badge=r.s("strBadge"), league_id=r.id_("idLeague"), league=r.s("strLeague"),
813                    season=r.s("strSeason"), group=r.s("strGroup"), form=r.s("strForm"),
814                    description=r.s("strDescription"), played=r.i("intPlayed"), won=r.i("intWin"),
815                    drawn=r.i("intDraw"), lost=r.i("intLoss"), goals_for=r.i("intGoalsFor"),
816                    goals_against=r.i("intGoalsAgainst"), goal_difference=r.i("intGoalDifference"),
817                    points=r.i("intPoints"), updated=r.ldt("dateUpdated"))

One row of a league table (lookuptable.php; v1 only).

Standing( *, raw: Mapping[str, str | None] = <factory>, id: int | None, rank: int | None, team_id: int | None, team: str | None, badge: str | None, league_id: int | None, league: str | None, season: str | None, group: str | None, form: str | None, description: str | None, played: int | None, won: int | None, drawn: int | None, lost: int | None, goals_for: int | None, goals_against: int | None, goal_difference: int | None, points: int | None, updated: datetime.datetime | None)
id: int | None
rank: int | None
team_id: int | None
team: str | None
badge: str | None
league_id: int | None
league: str | None
season: str | None
group: str | None
form: str | None
description: str | None
played: int | None
won: int | None
drawn: int | None
lost: int | None
goals_for: int | None
goals_against: int | None
goal_difference: int | None
points: int | None
updated: datetime.datetime | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Team(thesportsdb_client.ApiRecord):
308@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
309class Team(ApiRecord):
310    """A team. Search and list endpoints fill only some fields; a lookup by id fills them all."""
311
312    _summary = ("id", "name")
313    id: int | None  # idTeam
314    name: str | None  # strTeam
315    short_name: str | None  # strTeamShort, e.g. "ARS"
316    alternate_names: tuple[str, ...]  # strTeamAlternate, split on commas
317    keywords: tuple[str, ...]  # strKeywords, split on commas (nicknames)
318    sport: str | None  # strSport
319    gender: str | None  # strGender
320    country: str | None  # strCountry
321    location: str | None  # strLocation
322    formed_year: int | None  # intFormedYear
323    league_id: int | None  # idLeague: the main league
324    league: str | None  # strLeague
325    leagues: tuple[LeagueRef, ...]  # idLeague..idLeague7 with their names
326    division: str | None  # strDivision
327    venue_id: int | None  # idVenue
328    stadium: str | None  # strStadium
329    colours: tuple[str, ...]  # strColour1..3, hex like "#EF0107"
330    badge: str | None  # strBadge
331    logo: str | None  # strLogo
332    banner: str | None  # strBanner
333    equipment: str | None  # strEquipment: the current kit image
334    fanart: tuple[str, ...]  # strFanart1..4
335    descriptions: Mapping[str, str]  # strDescriptionEN, ...
336    socials: Socials
337    loved: int | None  # intLoved
338    is_locked: bool | None  # strLocked
339    api_football_id: int | None  # idAPIfootball
340    espn_id: str | None  # idESPN
341
342    @property
343    def description(self) -> str | None:
344        return self.descriptions.get("EN")
345
346    @classmethod
347    def _read(cls, r: Rec) -> dict[str, Any]:
348        leagues = []
349        for n in range(1, 8):
350            suffix = "" if n == 1 else str(n)
351            if (lid := r.id_(f"idLeague{suffix}")) is not None:
352                leagues.append(LeagueRef(lid, r.s(f"strLeague{suffix}")))
353        return dict(
354            id=r.id_("idTeam"), name=r.s("strTeam"), short_name=r.s("strTeamShort"),
355            alternate_names=r.csv("strTeamAlternate"), keywords=r.csv("strKeywords"), sport=r.s("strSport"),
356            gender=r.s("strGender"), country=r.s("strCountry"), location=r.s("strLocation"),
357            formed_year=r.year("intFormedYear"), league_id=r.id_("idLeague"), league=r.s("strLeague"),
358            leagues=tuple(leagues), division=r.s("strDivision"), venue_id=r.id_("idVenue"),
359            stadium=r.s("strStadium"), colours=r.numbered("strColour", 3), badge=r.s("strBadge"),
360            logo=r.s("strLogo"), banner=r.s("strBanner"), equipment=r.s("strEquipment"),
361            fanart=r.numbered("strFanart", 4), descriptions=MappingProxyType(r.descriptions()),
362            socials=Socials._read(r), loved=r.i("intLoved"), is_locked=r.locked(),
363            api_football_id=r.id_("idAPIfootball"), espn_id=r.s("idESPN"),
364        )

A team. Search and list endpoints fill only some fields; a lookup by id fills them all.

Team( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, short_name: str | None, alternate_names: tuple[str, ...], keywords: tuple[str, ...], sport: str | None, gender: str | None, country: str | None, location: str | None, formed_year: int | None, league_id: int | None, league: str | None, leagues: tuple[LeagueRef, ...], division: str | None, venue_id: int | None, stadium: str | None, colours: tuple[str, ...], badge: str | None, logo: str | None, banner: str | None, equipment: str | None, fanart: tuple[str, ...], descriptions: Mapping[str, str], socials: Socials, loved: int | None, is_locked: bool | None, api_football_id: int | None, espn_id: str | None)
id: int | None
name: str | None
short_name: str | None
alternate_names: tuple[str, ...]
keywords: tuple[str, ...]
sport: str | None
gender: str | None
country: str | None
location: str | None
formed_year: int | None
league_id: int | None
league: str | None
leagues: tuple[LeagueRef, ...]
division: str | None
venue_id: int | None
stadium: str | None
colours: tuple[str, ...]
badge: str | None
banner: str | None
equipment: str | None
fanart: tuple[str, ...]
descriptions: Mapping[str, str]
socials: Socials
loved: int | None
is_locked: bool | None
api_football_id: int | None
espn_id: str | None
description: str | None
342    @property
343    def description(self) -> str | None:
344        return self.descriptions.get("EN")
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class TimelineEntry(thesportsdb_client.ApiRecord):
856@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
857class TimelineEntry(ApiRecord):
858    """A goal, card or substitution during an event."""
859
860    _summary = ("event_id", "minute", "type", "player")
861    id: int | None  # idTimeline
862    event_id: int | None  # idEvent
863    event: str | None  # strEvent
864    date: Date | None  # dateEvent
865    season: str | None  # strSeason
866    minute: int | None  # intTime
867    period: str | None  # strPeriod
868    type: str | None  # strTimeline, e.g. Goal, Card, subst
869    detail: str | None  # strTimelineDetail
870    comment: str | None  # strComment
871    player_id: int | None  # idPlayer
872    player: str | None  # strPlayer
873    cutout: str | None  # strCutout
874    assist_id: int | None  # idAssist
875    assist: str | None  # strAssist
876    team_id: int | None  # idTeam
877    team: str | None  # strTeam
878    is_home: bool | None  # strHome
879    api_football_id: int | None  # idAPIfootball
880
881    @classmethod
882    def _read(cls, r: Rec) -> dict[str, Any]:
883        return dict(id=r.id_("idTimeline"), event_id=r.id_("idEvent"), event=r.s("strEvent"), date=r.d("dateEvent"),
884                    season=r.s("strSeason"), minute=r.i("intTime"), period=r.s("strPeriod"),
885                    type=r.s("strTimeline"), detail=r.s("strTimelineDetail"), comment=r.s("strComment"),
886                    player_id=r.id_("idPlayer"), player=r.s("strPlayer"), cutout=r.s("strCutout"),
887                    assist_id=r.id_("idAssist"), assist=r.s("strAssist"), team_id=r.id_("idTeam"),
888                    team=r.s("strTeam"), is_home=r.b("strHome"), api_football_id=r.id_("idAPIfootball"))

A goal, card or substitution during an event.

TimelineEntry( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, event: str | None, date: datetime.date | None, season: str | None, minute: int | None, period: str | None, type: str | None, detail: str | None, comment: str | None, player_id: int | None, player: str | None, cutout: str | None, assist_id: int | None, assist: str | None, team_id: int | None, team: str | None, is_home: bool | None, api_football_id: int | None)
id: int | None
event_id: int | None
event: str | None
date: datetime.date | None
season: str | None
minute: int | None
period: str | None
type: str | None
detail: str | None
comment: str | None
player_id: int | None
player: str | None
cutout: str | None
assist_id: int | None
assist: str | None
team_id: int | None
team: str | None
is_home: bool | None
api_football_id: int | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class TvListing(thesportsdb_client.ApiRecord):
910@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
911class TvListing(ApiRecord):
912    """One broadcast of an event on one channel."""
913
914    _summary = ("event_id", "channel", "timestamp")
915    id: int | None  # id
916    event_id: int | None  # idEvent
917    event: str | None  # strEvent
918    sport: str | None  # strSport
919    season: str | None  # strSeason
920    channel_id: int | None  # idChannel
921    channel: str | None  # strChannel, e.g. "TSN 1"
922    channel_logo: str | None  # strLogo
923    country: str | None  # strCountry: the channel's country
924    event_country: str | None  # strEventCountry
925    timestamp: DateTime | None  # strTimeStamp (capital S, space-separated): UTC
926    date: Date | None  # dateEvent
927    time: Time | None  # strTime
928    division: int | None  # intDivision
929    event_thumb: str | None  # strEventThumb
930    event_poster: str | None  # strEventPoster
931    event_banner: str | None  # strEventBanner
932    event_square: str | None  # strEventSquare
933
934    @classmethod
935    def _read(cls, r: Rec) -> dict[str, Any]:
936        return dict(id=r.id_("id"), event_id=r.id_("idEvent"), event=r.s("strEvent"), sport=r.s("strSport"),
937                    season=r.s("strSeason"), channel_id=r.id_("idChannel"), channel=r.s("strChannel"),
938                    channel_logo=r.s("strLogo"), country=r.s("strCountry"), event_country=r.s("strEventCountry"),
939                    timestamp=r.ts("strTimeStamp") or r.ts("strTimestamp"), date=r.d("dateEvent"),
940                    time=r.t("strTime"), division=r.i("intDivision"), event_thumb=r.s("strEventThumb"),
941                    event_poster=r.s("strEventPoster"), event_banner=r.s("strEventBanner"),
942                    event_square=r.s("strEventSquare"))

One broadcast of an event on one channel.

TvListing( *, raw: Mapping[str, str | None] = <factory>, id: int | None, event_id: int | None, event: str | None, sport: str | None, season: str | None, channel_id: int | None, channel: str | None, channel_logo: str | None, country: str | None, event_country: str | None, timestamp: datetime.datetime | None, date: datetime.date | None, time: datetime.time | None, division: int | None, event_thumb: str | None, event_poster: str | None, event_banner: str | None, event_square: str | None)
id: int | None
event_id: int | None
event: str | None
sport: str | None
season: str | None
channel_id: int | None
channel: str | None
country: str | None
event_country: str | None
timestamp: datetime.datetime | None
date: datetime.date | None
time: datetime.time | None
division: int | None
event_thumb: str | None
event_poster: str | None
event_banner: str | None
event_square: str | None
@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
class Venue(thesportsdb_client.ApiRecord):
633@dataclass(frozen=True, kw_only=True, eq=False, repr=False)
634class Venue(ApiRecord):
635    """A stadium, arena or circuit."""
636
637    _summary = ("id", "name")
638    id: int | None  # idVenue
639    name: str | None  # strVenue
640    alternate_name: str | None  # strVenueAlternate
641    sponsor_name: str | None  # strVenueSponsor
642    sport: str | None  # strSport
643    location: str | None  # strLocation
644    country: str | None  # strCountry
645    timezone: str | None  # strTimezone, as text
646    capacity: int | None  # intCapacity
647    formed_year: int | None  # intFormedYear
648    architect: str | None  # strArchitect
649    cost: str | None  # strCost, as text
650    map: str | None  # strMap: "lat, lon" or an image URL
651    thumb: str | None  # strThumb
652    logo: str | None  # strLogo
653    fanart: tuple[str, ...]  # strFanart1..4
654    creative_commons: bool | None  # strCreativeCommons
655    descriptions: Mapping[str, str]
656    socials: Socials
657    loved: int | None  # intLoved
658    is_locked: bool | None  # strLocked
659    duplicate_of: int | None  # idDupe
660
661    @property
662    def description(self) -> str | None:
663        return self.descriptions.get("EN")
664
665    @property
666    def coordinates(self) -> tuple[float, float] | None:
667        """``map`` as (latitude, longitude), when it holds coordinates."""
668        parts = (self.map or "").split(",")
669        if len(parts) != 2:
670            return None
671        try:
672            return float(parts[0]), float(parts[1])
673        except ValueError:
674            return None
675
676    @classmethod
677    def _read(cls, r: Rec) -> dict[str, Any]:
678        return dict(
679            id=r.id_("idVenue"), name=r.s("strVenue"), alternate_name=r.s("strVenueAlternate"),
680            sponsor_name=r.s("strVenueSponsor"), sport=r.s("strSport"), location=r.s("strLocation"),
681            country=r.s("strCountry"), timezone=r.s("strTimezone"), capacity=r.i("intCapacity"),
682            formed_year=r.year("intFormedYear"), architect=r.s("strArchitect"), cost=r.s("strCost"),
683            map=r.s("strMap"), thumb=r.s("strThumb"), logo=r.s("strLogo"), fanart=r.numbered("strFanart", 4),
684            creative_commons=r.b("strCreativeCommons"), descriptions=MappingProxyType(r.descriptions()),
685            socials=Socials._read(r), loved=r.i("intLoved"), is_locked=r.locked(), duplicate_of=r.id_("idDupe"),
686        )

A stadium, arena or circuit.

Venue( *, raw: Mapping[str, str | None] = <factory>, id: int | None, name: str | None, alternate_name: str | None, sponsor_name: str | None, sport: str | None, location: str | None, country: str | None, timezone: str | None, capacity: int | None, formed_year: int | None, architect: str | None, cost: str | None, map: str | None, thumb: str | None, logo: str | None, fanart: tuple[str, ...], creative_commons: bool | None, descriptions: Mapping[str, str], socials: Socials, loved: int | None, is_locked: bool | None, duplicate_of: int | None)
id: int | None
name: str | None
alternate_name: str | None
sponsor_name: str | None
sport: str | None
location: str | None
country: str | None
timezone: str | None
capacity: int | None
formed_year: int | None
architect: str | None
cost: str | None
map: str | None
thumb: str | None
fanart: tuple[str, ...]
creative_commons: bool | None
descriptions: Mapping[str, str]
socials: Socials
loved: int | None
is_locked: bool | None
duplicate_of: int | None
description: str | None
661    @property
662    def description(self) -> str | None:
663        return self.descriptions.get("EN")
coordinates: tuple[float, float] | None
665    @property
666    def coordinates(self) -> tuple[float, float] | None:
667        """``map`` as (latitude, longitude), when it holds coordinates."""
668        parts = (self.map or "").split(",")
669        if len(parts) != 2:
670            return None
671        try:
672            return float(parts[0]), float(parts[1])
673        except ValueError:
674            return None

map as (latitude, longitude), when it holds coordinates.

def sized(url: str | None, size: ImageSize) -> str | None:
104def sized(url: str | None, size: ImageSize) -> str | None:
105    """The same image at a smaller size.
106
107    Only ``r2.thesportsdb.com`` and ``www.thesportsdb.com/images/media/`` images support the
108    suffixes (others return 404, verified 5 Oct 2026); other URLs and None are returned unchanged.
109    """
110    if url is None or not url.startswith(("https://r2.thesportsdb.com/", "https://www.thesportsdb.com/images/media/")):
111        return url
112    for s in ImageSize:
113        url = url.removesuffix(s.value)
114    return url + size.value

The same image at a smaller size.

Only r2.thesportsdb.com and www.thesportsdb.com/images/media/ images support the suffixes (others return 404, verified 5 Oct 2026); other URLs and None are returned unchanged.