Skip to content

Commit

Permalink
Query Builder (#40)
Browse files Browse the repository at this point in the history
* Query Builder - First commit.  The goal of this is to make it easy to build complex filters to utilize the stats APIs in the nhl ecosystem

* QueryBuilder.  Adds more filter support for positions, franchise, game type.  Test coverage update


* Stat Query Builder.  Adds support for factExpressions base expressions passed via query context.  

Adds Decision, Experience, HomeRoad, Nationality, Status Query filters.

Renames stats.skater_stats_summary to stats.skater_stats_summary_simple.

README updated with latest query builder howtos.

QueryBase updates to have validate() abstract method so each filter can define its own validation rules. 

 QueryBuilder runs each validation upon build().

Updates QueryContext.  Adds .is_valid() for quick error checks.

Moves factCayenneExpr to QueryContext.
  • Loading branch information
coreyjs authored Feb 15, 2024
1 parent 6ef04b2 commit c7a48bf
Show file tree
Hide file tree
Showing 39 changed files with 974 additions and 1,045 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ archive.zip

# vim temporary files
*~
.*.sw?
.*.sw?

*/.DS_Store
241 changes: 208 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,211 @@
# NHL-API-PY


## This is being updated with the new, also undocumented, NHL API.

More endpoints will be flushed out and completed as they
are discovered. If you find any, please submit a PR.


## About

NHL-api-py is a Python package that provides a simple wrapper around the
NHL API, allowing you to easily access and retrieve NHL data in your Python
applications.

Note: This is very early, I created this to help me with some machine learning
Note: This is ~~very early~~ maturing, I created this to help me with some machine learning
projects around the NHL and the NHL data sets. Special thanks to https://github.com/erunion/sport-api-specifications/tree/master/nhl and https://gitlab.com/dword4/nhlapi/-/blob/master/stats-api.md.

### Developer Note: This is being updated with the new, also undocumented, NHL API.

## Usage
More endpoints will be flushed out and completed as they
are discovered. If you find any, open a ticket or post in the discussions tab. I would love to hear more.


---
## Contact
Im available on [Bluesky](https://bsky.app/profile/coreyjs.dev) for any questions or just general chats about enhancements.

---



# Usage
```python
from nhlpy import NHLClient

client = NHLClient()
# OR
client = NHLClient(verbose=True) # a tad more logging such as the URL being called
```
---
## Stats with QueryBuilder

The skater stats endpoint can be accessed using the new query builder. It should make
creating and understanding the queries a bit easier. Filters are being added as I go, and will match up
to what the NHL API will allow.



### Sorting
The sorting is a list of dictionaries similar to below. You can supply your own, otherwise it will
default to the default sort properties that the stat dashboard uses. All sorting defaults are found
in the `nhl-api-py/nhlpy/api/query/sorting/sorting_options.py` file.

<details>
<summary>Default Sorting</summary>

```python
skater_summary_default_sorting = [
{"property": "points", "direction": "DESC"},
{"property": "gamesPlayed", "direction": "ASC"},
{"property": "playerId", "direction": "ASC"},
]
```
</details>

---

### Report Types
The following report types are available. These are used to build the request url. So `/summary`, `/bios`, etc.

```bash
summary
bios
faceoffpercentages
faceoffwins
goalsForAgainst
realtime
penalties
penaltykill
penaltyShots
powerplay
puckPossessions
summaryshooting
percentages
scoringRates
scoringpergame
shootout
shottype
timeonice
```

### Available Filters

```python
from nhlpy.api.query.filters.franchise import FranchiseQuery
from nhlpy.api.query.filters.shoot_catch import ShootCatchesQuery
from nhlpy.api.query.filters.draft import DraftQuery
from nhlpy.api.query.filters.season import SeasonQuery
from nhlpy.api.query.filters.game_type import GameTypeQuery
from nhlpy.api.query.filters.position import PositionQuery, PositionTypes
from nhlpy.api.query.filters.status import StatusQuery
from nhlpy.api.query.filters.opponent import OpponentQuery
from nhlpy.api.query.filters.home_road import HomeRoadQuery
from nhlpy.api.query.filters.experience import ExperienceQuery
from nhlpy.api.query.filters.decision import DecisionQuery

filters = [
GameTypeQuery(game_type="2"),
DraftQuery(year="2020", draft_round="2"),
SeasonQuery(season_start="20202021", season_end="20232024"),
PositionQuery(position=PositionTypes.ALL_FORWARDS),
ShootCatchesQuery(shoot_catch="L"),
HomeRoadQuery(home_road="H"),
FranchiseQuery(franchise_id="1"),
StatusQuery(is_active=True) #for active players OR for HOF players StatusQuery(is_hall_of_fame=True),
OpponentQuery(opponent_franchise_id="2"),
ExperienceQuery(is_rookie=True), # for rookies || ExperienceQuery(is_rookie=False) #for veteran
DecisionQuery(decision="W") # OR DecisionQuery(decision="L") OR DecisionQuery(decision="O")
]
```


### Stats Endpoints (In development)
### Example
```python
from nhlpy.api.query.builder import QueryBuilder, QueryContext
from nhlpy.nhl_client import NHLClient
from nhlpy.api.query.filters.draft import DraftQuery
from nhlpy.api.query.filters.season import SeasonQuery
from nhlpy.api.query.filters.game_type import GameTypeQuery
from nhlpy.api.query.filters.position import PositionQuery, PositionTypes

client = NHLClient(verbose=True)

filters = [
GameTypeQuery(game_type="2"),
DraftQuery(year="2020", draft_round="2"),
SeasonQuery(season_start="20202021", season_end="20232024"),
PositionQuery(position=PositionTypes.ALL_FORWARDS)
]

query_builder = QueryBuilder()
query_context: QueryContext = query_builder.build(filters=filters)

data = client.stats.skater_stats_with_query_context(
report_type='summary',
query_context=query_context,
aggregate=True
)
```

### Granular Filtering
Each API request uses an additional query parameter called `factCayenneExp`. This defaults to `gamesPlayed>=1`
but can be overridden by setting the `fact_query` parameter in the `QueryContextObject` object. These can
be combined together with `and` to create a more complex query. It supports `>`, `<`, `>=`, `<=`. For example: `shootingPct>=0.01 and timeOnIcePerGame>=60 and faceoffWinPct>=0.01 and shots>=1`


This should support the following filters:

- `gamesPlayed`
- `points`
- `goals`
- `pointsPerGame`
- `penaltyMinutes`
- `plusMinus`
- `ppGoals` # power play goals
- `evGoals` # even strength goals
- `pointsPerGame`
- `penaltyMinutes`
- `evPoints` # even strength points
- `ppPoints` # power play points
- `gameWinningGoals`
- `otGoals`
- `shPoints` # short handed points
- `shGoals` # short handed goals
- `shootingPct`
- `timeOnIcePerGame`
- `faceoffWinPct`
- `shots`

```python
.....
query_builder = QueryBuilder()
query_context: QueryContext = query_builder.build(filters=filters)

query_context.fact_query = "gamesPlayed>=1 and goals>=10" # defaults to gamesPlayed>=1

data = client.stats.skater_stats_with_query_context(
report_type='summary',
query_context=query_context,
aggregate=True
)
```


### Invalid Query / Errors

The `QueryContext` object will hold the result of the built query with the supplied queries.
In the event of an invalid query (bad data, wrong option, etc), the `QueryContext` object will
hold all the errors that were encountered during the build process. This should help in debugging.

You can quickly check the `QueryContext` object for errors by calling `query_context.is_valid()`. Any "invalid" filters
will be removed from the output query, but anything that is still valid will be included.

```python
...
query_context: QueryContext = query_builder.build(filters=filters)
query_context.is_valid() # False if any of the filters fails its validation check
query_context.errors
```

---

## Additional Stats Endpoints (In development)

```python

Expand All @@ -39,33 +217,21 @@ client.stats.club_stats_season(team_abbr="BUF") # kinda weird endpoint.
client.stats.player_career_stats(player_id="8478402")

# Team Summary Stats.
# These have lots of available parameters. You can also tap into the apache cayenne expressions to build custom
# queries, if you have that knowledge.
# These have lots of available parameters. You can also tap into the apache cayenne expressions to build custom
# queries, if you have that knowledge.
client.stats.team_summary(start_season="20202021", end_season="20212022", game_type_id=2)
client.stats.team_summary(start_season="20202021", end_season="20212022")

###

# Skater Summary Stats.
# Queries for skaters for year ranges, filterable down by franchise.
# Queries for skaters for year ranges, filterable down by franchise.
client.stats.skater_stats_summary(start_season="20232024", end_season="20232024")
client.stats.skater_stats_summary(franchise_id=10, start_season="20232024", end_season="20232024")

# skater_stats_summary_by_expression is more advanced method. It allows for more direct manipulation of the query and
# the cayenne expression clauses.
sort_expr = [
{"property": "points", "direction": "DESC"},
{"property": "gamesPlayed", "direction": "ASC"},
{"property": "playerId", "direction": "ASC"},
]
expr = "gameTypeId=2 and seasonId<=20232024 and seasonId>=20222023"
client.stats.skater_stats_summary_by_expression(default_cayenne_exp=expr, sort_expr=sort_expr)

###

```
---


### Schedule Endpoints
## Schedule Endpoints

```python
client.schedule.get_schedule(date="2021-01-13")
Expand All @@ -82,7 +248,9 @@ client.schedule.get_season_schedule(team_abbr="BUF", season="20212022")
client.schedule.schedule_calendar(date="2023-11-23")
```

### Standings Endpoints
---

## Standings Endpoints

```python
client.standings.get_standings()
Expand All @@ -94,17 +262,19 @@ client.standings.get_standings(season="202222023")
# for less API calls since this only changes yearly.
client.standings.season_standing_manifest()
```
---

### Teams Endpoints
## Teams Endpoints

```python
client.teams.teams_info() # returns id + abbrevation + name of all teams

client.teams.team_stats_summary(lang="en") # I honestly dont know. This is missing teams and has teams long abandoned.
```

---

### Game Center
## Game Center
```python
client.game_center.boxscore(game_id="2023020280")

Expand All @@ -115,8 +285,9 @@ client.game_center.landing(game_id="2023020280")
client.game_center.score_now()
```

---

### Misc Endpoints
## Misc Endpoints
```python
client.misc.glossary()

Expand All @@ -130,7 +301,7 @@ client.misc.draft_year_and_rounds()
```

---
### Insomnia Rest Client Export
## Insomnia Rest Client Export

[Insomnia Rest Client](https://insomnia.rest) is a great tool for testing

Expand All @@ -142,7 +313,7 @@ into the client and use it to test the endpoints. I will be updating this as I
- - -


### Developers
## Developers

1) Install [Poetry](https://python-poetry.org/docs/#installing-with-the-official-installer)

Expand All @@ -157,6 +328,10 @@ or using pipx

3) `poetry shell`


### Build Pipeline
The build pipeline will run `black`, `ruff`, and `pytest`. Please make sure these are passing before submitting a PR.

```python

$ poetry shell
Expand Down
Loading

0 comments on commit c7a48bf

Please sign in to comment.