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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 73 additions & 14 deletions docs/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ This document is the authoritative public API contract for the
`python-mlb-statsapi` **1.x** series.

It defines which package-root symbols, constructor signatures, exception and
warning relationships, Session ownership rules, and `Mlb` endpoint methods are
supported after version 1.0. Maintainers should use this document when deciding
whether a change is a patch, a minor release, or a major release.
warning relationships, resource ownership rules, and `Mlb` and `AsyncMlb`
endpoint methods are supported after version 1.0. Maintainers should use this
document when deciding whether a change is a patch, a minor release, or a major
release.

This package is an unofficial wrapper for the MLB Stats API and is not
affiliated with Major League Baseball.
Expand Down Expand Up @@ -78,9 +79,10 @@ from mlbstatsapi import (
)
```

The symbols above are available in every install. `AsyncMlbDataAdapter` is
equally public, but it resolves only when the optional `async` extra is
installed; see [Optional async support](#optional-async-support).
The symbols above are available in every install. `AsyncMlb` and
`AsyncMlbDataAdapter` are equally public, but they resolve only when the
optional `async` extra is installed; see
[Optional async support](#optional-async-support).

### Classification of package-root symbols

Expand All @@ -91,6 +93,7 @@ resolving it needs an optional dependency.
| Symbol | Status | Availability |
| --- | --- | --- |
| `Mlb` | Public and stable in 1.x | Always available |
| `AsyncMlb` | Public and stable in 1.x | Requires the optional `async` extra |
| `MlbDataAdapter` | Public and stable in 1.x | Always available |
| `AsyncMlbDataAdapter` | Public and stable in 1.x | Requires the optional `async` extra |
| `MlbResult` | Public and stable in 1.x | Always available |
Expand All @@ -104,10 +107,11 @@ resolving it needs an optional dependency.
| `return_splits` | Public legacy helper, stable in 1.x but not preferred for new code | Always available |
| `get_stat_attributes` | Public legacy helper, stable in 1.x but not preferred for new code | Always available |

`AsyncMlbDataAdapter` is supported 1.x API on the same terms as the synchronous
symbols: it will not be removed or renamed during the series, and its documented
behavior stays compatible. Only its availability is conditional, because its
HTTP dependency ships with the `async` extra. See
`AsyncMlb` and `AsyncMlbDataAdapter` are supported 1.x API on the same terms as
the synchronous symbols: they will not be removed or renamed during the
series, and their documented behavior stays compatible. Only their
availability is conditional, because their HTTP dependency ships with the
`async` extra. See
[Optional async support](#optional-async-support).

No package-root symbol is marked deprecated in version 1.0. Deprecation requires
Expand Down Expand Up @@ -150,9 +154,9 @@ accidental submodule names (for example, a documented deprecation period).

## Optional async support

`AsyncMlbDataAdapter` is a public package-root symbol, like `MlbDataAdapter`,
and appears in the classification table above. Its HTTP dependency is optional
and installed with the `async` extra:
`AsyncMlb` and `AsyncMlbDataAdapter` are public package-root symbols, like
`Mlb` and `MlbDataAdapter`, and appear in the classification table above. Their
HTTP dependency is optional and installed with the `async` extra:

```bash
pip install "python-mlb-statsapi[async]"
Expand All @@ -161,7 +165,7 @@ pip install "python-mlb-statsapi[async]"
With the extra installed:

```python
from mlbstatsapi import AsyncMlbDataAdapter
from mlbstatsapi import AsyncMlb, AsyncMlbDataAdapter
```

Async symbols are resolved on first access, so the optional dependency is not
Expand Down Expand Up @@ -228,6 +232,61 @@ Session. Most endpoint methods use `v1`. `get_game` uses the `v1.1` live feed
endpoint. Standalone `MlbDataAdapter(ver="v1")` and
`MlbDataAdapter(ver="v1.1")` remain supported.

## AsyncMlb public client

`AsyncMlb` is the public asynchronous client and requires the optional `async`
extra.

### Constructor

```text
AsyncMlb(
hostname="statsapi.mlb.com",
logger=None,
timeout=(3.05, 30.0),
client=None,
*,
strict_http=True,
)
```

Parameter order and default values above are part of the API.
`strict_http` is keyword-only.

### Lifecycle

* `async with AsyncMlb(...) as mlb` returns the `AsyncMlb` instance itself
* `AsyncMlb.__aexit__` awaits cleanup
* Explicit cleanup with `await mlb.aclose()` is supported
* Repeated `aclose()` calls are safe
* Library-owned async clients are closed
* Caller-injected async clients remain caller-owned and open

### Concurrency

One `AsyncMlb` instance supports concurrent in-flight requests on the same
event loop. Concurrency is caller-controlled. Cross-event-loop use is not
promised.

### Endpoint methods

The currently supported awaitable endpoint methods are:

```text
get_team(team_id: int, **params)
get_teams(sport_id: int = 1, **params)
get_person(player_id: int, **params)
get_people(sport_id: int = 1, **params)
get_schedule(
date: str = None,
start_date: str = None,
end_date: str = None,
sport_id: int = 1,
team_id: int = None,
**params,
)
```

## Low-level adapter

`MlbDataAdapter` is the public low-level HTTP adapter.
Expand Down
19 changes: 14 additions & 5 deletions mlbstatsapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,29 @@
# first access keeps async functionality discoverable from the package root
# while the missing-dependency error surfaces only when async is actually
# requested. See docs/public-api.md.
_LAZY_ASYNC_EXPORTS = ("AsyncMlbDataAdapter",)
_LAZY_ASYNC_EXPORTS = (
"AsyncMlb",
"AsyncMlbDataAdapter",
)


def __getattr__(name: str):
if name in _LAZY_ASYNC_EXPORTS:
if name == "AsyncMlb":
from .async_mlb import AsyncMlb

globals()["AsyncMlb"] = AsyncMlb
return AsyncMlb

if name == "AsyncMlbDataAdapter":
from .async_mlb_dataadapter import AsyncMlbDataAdapter

# Cache on the module so later attribute access is an ordinary lookup.
globals()["AsyncMlbDataAdapter"] = AsyncMlbDataAdapter
return AsyncMlbDataAdapter

raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
raise AttributeError(
f"module {__name__!r} has no attribute {name!r}"
)


def __dir__() -> list[str]:
# Keeps the lazy async names discoverable without importing HTTPX.
return sorted(set(globals()) | set(_LAZY_ASYNC_EXPORTS))
Empty file.
22 changes: 22 additions & 0 deletions mlbstatsapi/_helpers/schedule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
def build_schedule_params(
date: str | None = None,
start_date: str | None = None,
end_date: str | None = None,
sport_id: int = 1,
team_id: int | None = None,
**params,
) -> dict | None:
if start_date and end_date:
params["startDate"] = start_date
params["endDate"] = end_date
elif date and not (start_date or end_date):
params["date"] = date
elif "gamePks" not in params:
return None

if team_id:
params["teamId"] = team_id

params["sportId"] = sport_id

return params
174 changes: 174 additions & 0 deletions mlbstatsapi/async_mlb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# mlbstatsapi/async_mlb.py

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from ._helpers.schedule import build_schedule_params
from ._parsers.people import parse_person, parse_people
from ._parsers.schedules import parse_schedule
from ._parsers.teams import parse_team, parse_teams
from .async_mlb_dataadapter import AsyncMlbDataAdapter
from .mlb_dataadapter import DEFAULT_TIMEOUT, TimeoutType
from .models.people import Person
from .models.schedules import Schedule
from .models.teams import Team

if TYPE_CHECKING:
import httpx


class AsyncMlb:
"""Asynchronous client for the MLB Stats API."""

def __init__(
self,
hostname: str = "statsapi.mlb.com",
logger: logging.Logger | None = None,
timeout: TimeoutType = DEFAULT_TIMEOUT,
client: "httpx.AsyncClient | None" = None,
*,
strict_http: bool = True,
):
self._logger = logger or logging.getLogger(__name__)

self._mlb_adapter_v1 = AsyncMlbDataAdapter(
hostname=hostname,
ver="v1",
logger=self._logger,
timeout=timeout,
client=client,
strict_http=strict_http,
)

async def aclose(self) -> None:
"""Close library-owned async resources."""
await self._mlb_adapter_v1.aclose()

async def __aenter__(self) -> "AsyncMlb":
return self

async def __aexit__(
self,
exc_type,
exc,
traceback,
) -> None:
try:
await self.aclose()
except BaseException:
# Cleanup must not replace an exception or cancellation that
# already occurred inside the async context.
if exc is None:
raise

self._logger.exception(
"AsyncMlb cleanup failed while preserving the original exception"
)

async def get_team(
self,
team_id: int,
**params,
) -> Team | None:
mlb_data = await self._mlb_adapter_v1.get(
endpoint=f"teams/{team_id}",
ep_params=params,
)

if 400 <= mlb_data.status_code <= 499:
return None

return parse_team(mlb_data.data)

async def get_teams(
self,
sport_id: int = 1,
**params,
) -> list[Team]:
"""Return every Team for a sport id.

Async counterpart of ``Mlb.get_teams``; see that method for the
supported keyword parameters.
"""
params["sportId"] = sport_id

mlb_data = await self._mlb_adapter_v1.get(
endpoint="teams",
ep_params=params,
)

if 400 <= mlb_data.status_code <= 499:
return []

return parse_teams(mlb_data.data)

async def get_person(
self,
player_id: int,
**params,
) -> Person | None:
mlb_data = await self._mlb_adapter_v1.get(
endpoint=f"people/{player_id}",
ep_params=params,
)

if 400 <= mlb_data.status_code <= 499:
return None

return parse_person(mlb_data.data)

async def get_people(
self,
sport_id: int = 1,
**params,
) -> list[Person]:
"""Return every player for a sport id.

Async counterpart of ``Mlb.get_people``, which reads the
``sports/{sport_id}/players`` endpoint rather than ``people``.
"""
mlb_data = await self._mlb_adapter_v1.get(
endpoint=f"sports/{sport_id}/players",
ep_params=params,
)

if 400 <= mlb_data.status_code <= 499:
return []

return parse_people(mlb_data.data)


async def get_schedule(
self,
date: str = None,
start_date: str = None,
end_date: str = None,
sport_id: int = 1,
team_id: int = None,
**params,
) -> Schedule | None:

params = build_schedule_params(
date=date,
start_date=start_date,
end_date=end_date,
sport_id=sport_id,
team_id=team_id,
**params,
)

if params is None:
return None

mlb_data = await self._mlb_adapter_v1.get(
endpoint="schedule",
ep_params=params,
)


if 400 <= mlb_data.status_code <= 499:
return None

return parse_schedule(mlb_data.data)
Loading
Loading