A synchronous and asynchronous client for the unofficial NHL API
Project description
slapshot
Purpose
slapshot is a synchronous and asynchronous client for the unofficial NHL API. It covers the four public NHL services behind a single, typed interface: api-web.nhle.com, api.nhle.com/stats/rest, records.nhl.com, and the player search at search.d3.nhle.com.
Why slapshot?
The NHL exposes a large, undocumented surface of endpoints spread across several hosts, each with its own path conventions, query parameters, and payload shapes. slapshot wraps them behind one typed interface, so the caller works with the data rather than with URL construction and response parsing.
Stable payloads decode into msgspec structs with snake_case attributes, while volatile payloads come back as decoded JSON. Decoding ignores unknown fields, so new NHL fields never break existing code. Every URL template lives in an enum, so no path is ever built from a loose string.
How It Works
slapshot has four layers:
- Client (
NHL,AsyncNHL): owns thehttpxtransport and exposes one attribute per resource. - Resources (
client.standings,client.players, ...): group related endpoints into methods. - Request builders (
slapshot.resources.*): validate arguments and produce aRequestof URL and query parameters. - Endpoints (
WebEndpoint,StatsEndpoint,RecordsEndpoint,SearchEndpoint): hold every URL template as aStrEnum.
The synchronous NHL and asynchronous AsyncNHL clients expose the same resources and the same method names. AsyncNHL mirrors NHL; its methods are awaitable.
Core Features
- Synchronous and Asynchronous: an identical API on
NHLandAsyncNHL, chosen per call site. - Typed Models: stable payloads decode into
msgspecstructs; volatile payloads return raw JSON. - Forward Compatible: decoding ignores unknown response fields, so new NHL fields never break existing code.
- No Loose Paths: every endpoint is a
StrEnumtemplate, and slapshot validates identifiers, seasons, and dates before it sends a request. - Bring Your Own Transport: inject a preconfigured
httpxclient for proxies, retries, or custom headers. - Typed Errors: a single
NHLErrorhierarchy covers transport, HTTP status, decode, and parameter failures.
Backing APIs
| Service | Base URL | Resources |
|---|---|---|
| Web | https://api-web.nhle.com/v1 |
standings, schedule, games, players, teams, draft |
| Stats | https://api.nhle.com/stats/rest |
stats |
| Records | https://records.nhl.com/site/api |
records |
| Search | https://search.d3.nhle.com/api/v1 |
players.search |
Installation
Prerequisites
- Python 3.11 or newer
- uv (recommended) or
pip
From PyPI
uv add slapshot
Or with pip:
pip install slapshot
From Source
git clone https://github.com/braycarlson/slapshot
cd slapshot
uv sync
Usage
Runnable scripts for every resource live in examples/. Run one with uv run python examples/quickstart.py.
Synchronous
from slapshot import NHL, Milestone, Team
with NHL() as client:
standings = client.standings.now()
print(standings.standings[0].team_name.default)
player = client.players.landing(8478402)
print(player.first_name.default, player.last_name.default)
skaters = client.stats.skaters(season=20252026, limit=5)
milestone = client.records.milestone(Milestone.GOAL_CAREER_500)
roster = client.teams.roster(Team.EDMONTON)
Asynchronous
import asyncio
from slapshot import AsyncNHL, GameType, Team
async def main() -> None:
async with AsyncNHL() as client:
schedule = await client.schedule.club_week(Team.EDMONTON)
log = await client.players.game_log(8478402, season=20252026, game_type=GameType.REGULAR)
results = await client.players.search('mcdavid', limit=3)
asyncio.run(main())
Resources
Every resource comes in both a synchronous and an asynchronous form, under the same attribute name and with the same method signatures.
| Resource | Backing API | Methods |
|---|---|---|
client.standings |
api-web.nhle.com | now, by_date, seasons |
client.schedule |
api-web.nhle.com | now, by_date, club_week, club_month, club_season |
client.games |
api-web.nhle.com | landing, boxscore, play_by_play, right_rail, story, scores, scoreboard |
client.players |
api-web.nhle.com, search.d3.nhle.com | landing, game_log, spotlight, search, skater_leaders, goalie_leaders |
client.teams |
api-web.nhle.com | roster, roster_seasons, prospects, club_stats, club_stats_seasons |
client.draft |
api-web.nhle.com | rankings, picks |
client.stats |
api.nhle.com/stats/rest | skaters, goalies, teams, report, config, franchises, seasons, glossary |
client.records |
records.nhl.com | attendance, draft, franchises, franchise_details, franchise_season_records, franchise_season_results, franchise_team_totals, franchise_goalie_records, franchise_skater_records, milestone, officials, players, playoff_series, all_time_record, trophies |
Models
Stable payloads decode into msgspec structs with snake_case attributes: StandingsResponse / StandingsTeam, Roster / RosterPlayer, PlayerLanding, PlayerSearchResult, and the ResultSet envelope (data, total) shared by every stats and records report. Volatile payloads (gamecenter, play-by-play, schedules, draft, leaders, config) return decoded JSON as-is.
A LocalizedName wraps localized text; its default attribute holds the English value.
The raw payload for any endpoint remains available through the request builders:
from slapshot.resources.standings import standings_now
request = standings_now()
raw = client.get(request)
Every URL template lives in slapshot.endpoints as a StrEnum (WebEndpoint, StatsEndpoint, RecordsEndpoint, and SearchEndpoint), so no path is built from a loose string:
from slapshot import WebEndpoint, request_build
url = WebEndpoint.PLAYER_LANDING.format(player=8478402)
request = request_build(url)
raw = client.get(request)
Enums
Team, GameType, Language, Milestone, and SortDirection are StrEnum / IntEnum values accepted anywhere the corresponding string or integer is.
Errors
Every exception inherits from slapshot.NHLError.
- HTTP status:
BadRequestError,UnauthorizedError,ForbiddenError,NotFoundError,RateLimitError,ServerError, or the baseAPIStatusError. - Transport:
NHLTimeoutErrororNHLConnectionError. - Response:
UnexpectedContentTypeErrorfor a non-JSON response, andResponseDecodeErrorwhen a body cannot be decoded or does not match the expected model. - Parameters:
InvalidParameterError, which is also aValueError, so existingexcept ValueErrorhandlers keep working.
Detecting Unknown Fields
By default, decoding ignores fields the models do not define. To surface them instead, so nothing is silently dropped, construct a client with warn_unknown_fields=True. Any typed response whose payload carries an undefined field emits an UnknownFieldWarning naming the field paths.
from slapshot import NHL
with NHL(warn_unknown_fields=True) as client:
client.standings.now()
The unknown_fields helper performs the same check directly, and tests/test_drift.py (marked network) runs it against the live API for every typed endpoint:
from slapshot import unknown_fields
from slapshot.models import StandingsResponse
missing = unknown_fields(raw_payload, StandingsResponse)
client.get(request) always returns the raw payload, so no data is lost regardless of the models.
Documentation
Full documentation lives in docs/, built with MkDocs. Serve it locally:
uv run --with mkdocs-material mkdocs serve
- Installation: How to install slapshot and its dependencies
- Quickstart: A first synchronous and asynchronous request
- Client: Constructing
NHLandAsyncNHL, timeouts, and custom transports - Resources: Every resource and method, with parameters and return types
- Models: The
msgspecstructs returned by typed endpoints - Enums:
Team,GameType,Language,Milestone, andSortDirection - Errors: The exception hierarchy and when each error is raised
- Raw Requests: Dropping down to request builders and endpoints
Development
uv sync installs the development dependencies. Three tools check the project:
| Command | Purpose |
|---|---|
uv run ruff check |
Lint |
uv run ty check |
Type check |
uv run python -m pytest |
Run the test suite |
Tests marked network hit the live NHL API; pytest deselects them by default. Run them with uv run python -m pytest -m network.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file slapshot-0.1.0.tar.gz.
File metadata
- Download URL: slapshot-0.1.0.tar.gz
- Upload date:
- Size: 20.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9221465468c8613b189e8127428a77227855da87227c76dc244c37e0358977ac
|
|
| MD5 |
92712b166fb81e08ba6537ae19ed8af9
|
|
| BLAKE2b-256 |
e864f315fd38c951f277dec8cef0ab0a85086fccc875146ccd1df5231515fc99
|
File details
Details for the file slapshot-0.1.0-py3-none-any.whl.
File metadata
- Download URL: slapshot-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.26 {"installer":{"name":"uv","version":"0.11.26","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7d4b7884f963e28e4e629c1ce112c17339021e2b9bf0bf79ed9db662fab5b2a2
|
|
| MD5 |
0de996e5e0e627a274fd2a4b47949d6a
|
|
| BLAKE2b-256 |
6470a9ac57ff7e0ef22ee7142988879ffedf22f53b0305c856a72757ccf6a65a
|