Skip to content

Commit

Permalink
QueryBuilder. Adds more filter support for positions, franchise, game…
Browse files Browse the repository at this point in the history
… type. Test coverage update
  • Loading branch information
coreyjs committed Feb 14, 2024
1 parent 939a6a9 commit fd91cfd
Show file tree
Hide file tree
Showing 16 changed files with 292 additions and 23 deletions.
73 changes: 64 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,22 +49,77 @@ client.stats.team_summary(start_season="20202021", end_season="20212022")
# 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")
```

### 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.

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
penaltie
penaltykill
penaltyShots
powerplay
puckPossessions
summaryshooting
percentages
scoringRates
scoringpergame
shootout
shottype
timeonice
```

### Available Filters

- Draft Year and Round
- Season Ranges
- Game Type (1=pre, 2=regular, 3=playoffs)
- Shoots/Catches (L, R)
- Franchise
- Position

```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)

# 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 = [
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)

###

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)

client.stats.skater_stats_with_query_context(
query_context=query_context,
sort_expr=sort_expr,
aggregate=True
)
```


### Schedule Endpoints

```python
Expand Down
1 change: 1 addition & 0 deletions nhl_api_2_3_0.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion nhlpy/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Should this be driven by the main pyproject.toml file? yes, is it super convoluted? yes, can it wait? sure

__version__ = "2.2.7"
__version__ = "2.3.0"
33 changes: 32 additions & 1 deletion nhlpy/api/query/builder.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,33 @@
from typing import List
import logging

from nhlpy.api.query.filters import QueryBase


class QueryContext:
def __init__(self, query: str, filters: List[QueryBase]):
self.query_str = query
self.filters = filters


class QueryBuilder:
pass
def __init__(self, verbose: bool = False):
self._verbose = verbose
if self._verbose:
logging.basicConfig(level=logging.INFO)

def build(self, filters: List[QueryBase]) -> QueryContext:
output: str = ""
for f in filters:
if not isinstance(f, QueryBase):
if self._verbose:
logging.info(f"Input filter is not of type QueryBase: {f.__name__}")

continue

_q = f.to_query()
output += f"{_q} and "
else:
output = output[:-5]

return QueryContext(query=output, filters=filters)
2 changes: 1 addition & 1 deletion nhlpy/api/query/filters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@

class QueryBase(ABC):
@abstractmethod
def to_query(self):
def to_query(self) -> str:
pass
10 changes: 10 additions & 0 deletions nhlpy/api/query/filters/franchise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from nhlpy.api.query.builder import QueryBase


class FranchiseQuery(QueryBase):
def __init__(self, franchise_id: str):
self.franchise_id = franchise_id
self._franchise_q = "franchiseId"

def to_query(self) -> str:
return f"{self._franchise_q}={self.franchise_id}"
10 changes: 10 additions & 0 deletions nhlpy/api/query/filters/game_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from nhlpy.api.query.builder import QueryBase


class GameTypeQuery(QueryBase):
def __init__(self, game_type: str):
self.game_type = game_type
self._game_type_q = "gameTypeId"

def to_query(self) -> str:
return f"{self._game_type_q}={self.game_type}"
28 changes: 28 additions & 0 deletions nhlpy/api/query/filters/position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from enum import Enum

from nhlpy.api.query.builder import QueryBase


class PositionTypes(str, Enum):
ALL_FORWARDS = "F"
CENTER = "C"
LEFT_WING = "L"
RIGHT_WING = "R"
DEFENSE = "D"


class PositionQuery(QueryBase):
def __init__(self, position: PositionTypes):
self.position = position
self._position_q = "positionCode"

def to_query(self) -> str:
# All forwards require an OR clause
if self.position == PositionTypes.ALL_FORWARDS:
return (
f"({self._position_q}='{PositionTypes.LEFT_WING.value}' "
f"or {self._position_q}='{PositionTypes.RIGHT_WING.value}' "
f"or {self._position_q}='{PositionTypes.CENTER.value}')"
)

return f"{self._position_q}='{self.position.value}'"
7 changes: 5 additions & 2 deletions nhlpy/api/query/filters/shoot_catch.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from nhlpy.api.query.builder import QueryBase


class ShootCatches(QueryBase):
class ShootCatchesQuery(QueryBase):
def __init__(self, shoot_catch: str):
"""
Shoot / catch filter. L or R, for both I believe its nothing.
:param shoot_catch: L, R
"""
self.shoot_catch = shoot_catch
self.shoot_catch_q = "shootsCatches"

def to_query(self) -> str:
return f"{self.shoot_catch_q}={self.shoot_catch}"
20 changes: 12 additions & 8 deletions nhlpy/api/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import json
from typing import List


from nhlpy.api.query.builder import QueryContext
from nhlpy.http_client import HttpClient


Expand Down Expand Up @@ -163,18 +163,17 @@ def skater_stats_summary(
"data"
]

def skater_stats_summary_by_expression(
def skater_stats_with_query_context(
self,
cayenne_exp: str,
query_context: QueryContext,
report_type: str,
sort_expr: List[dict],
aggregate: bool = False,
start: int = 0,
limit: int = 70,
fact_cayenne_exp: str = "gamesPlayed>=1",
) -> dict:
"""
A more bare bones / raw version of skater_stats_summary. This allows for more flexibility in the query params.
You must supply your own cayenne expressions and sort expressions.
example:
sort_expr = [
Expand All @@ -185,6 +184,10 @@ def skater_stats_summary_by_expression(
cayenne_exp = "gameTypeId=2 and seasonId<=20232024 and seasonId>=20232024"
client.stats.skater_stats_summary_by_expression(cayenne_exp=expr, sort_expr=sort_expr)
:param report_type: summary, bios, faceoffpercentages, faceoffwins, goalsForAgainst, realtime, penalties,
penaltykill, penaltyShots, powerplay, puckPossessions, summaryshooting, percentages, scoringRates,
scoringpergame, shootout, shottype, timeonice
:param query_context:
:param aggregate: bool - If doing multiple years, you can choose to aggreate the date per player,
or have separate entries for each one.
:param sort_expr: A list of key/value pairs for sort criteria. As used in skater_stats_summary(), this is
Expand All @@ -197,7 +200,6 @@ def skater_stats_summary_by_expression(
:param start:
:param limit:
:param fact_cayenne_exp:
:param default_cayenne_exp:
:return:
"""
q_params = {
Expand All @@ -208,5 +210,7 @@ def skater_stats_summary_by_expression(
"factCayenneExp": fact_cayenne_exp,
}
q_params["sort"] = urllib.parse.quote(json.dumps(sort_expr))
q_params["cayenneExp"] = cayenne_exp
return self.client.get_by_url("https://api.nhle.com/stats/rest/en/skater/summary", query_params=q_params).json()
q_params["cayenneExp"] = query_context.query_str
return self.client.get_by_url(
f"https://api.nhle.com/stats/rest/en/skater/{report_type}", query_params=q_params
).json()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "nhl-api-py"
version = "2.2.7"
version = "2.3.0"
description = "NHL API. For standings, team stats, outcomes, player information. Contains each individual API endpoint as well as convience methods for easy data loading in Pandas or any ML applications."
authors = ["Corey Schaf <[email protected]>"]
readme = "README.md"
Expand Down
6 changes: 6 additions & 0 deletions tests/query/filters/test_franchise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from nhlpy.api.query.filters.franchise import FranchiseQuery


def test_franchise_query():
franchise_query = FranchiseQuery(franchise_id="1")
assert franchise_query.to_query() == "franchiseId=1"
11 changes: 11 additions & 0 deletions tests/query/filters/test_game_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from nhlpy.api.query.filters.game_type import GameTypeQuery


def test_game_type_preseason():
game_type = GameTypeQuery(game_type="1")
assert game_type.to_query() == "gameTypeId=1"


def test_game_type_regular():
game_type = GameTypeQuery(game_type="2")
assert game_type.to_query() == "gameTypeId=2"
26 changes: 26 additions & 0 deletions tests/query/filters/test_position.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from nhlpy.api.query.filters.position import PositionQuery, PositionTypes


def test_centers():
position = PositionQuery(position=PositionTypes.CENTER)
assert position.to_query() == "positionCode='C'"


def test_left_wings():
position = PositionQuery(position=PositionTypes.LEFT_WING)
assert position.to_query() == "positionCode='L'"


def test_right_wings():
position = PositionQuery(position=PositionTypes.RIGHT_WING)
assert position.to_query() == "positionCode='R'"


def test_forwards():
position = PositionQuery(position=PositionTypes.ALL_FORWARDS)
assert position.to_query() == "(positionCode='L' or positionCode='R' or positionCode='C')"


def test_defense():
position = PositionQuery(position=PositionTypes.DEFENSE)
assert position.to_query() == "positionCode='D'"
11 changes: 11 additions & 0 deletions tests/query/filters/test_shoot_catch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from nhlpy.api.query.filters.shoot_catch import ShootCatchesQuery


def test_shoot_catch_l():
shoot_catch = ShootCatchesQuery(shoot_catch="L")
assert shoot_catch.to_query() == "shootsCatches=L"


def test_shoot_catch_r():
shoot_catch = ShootCatchesQuery(shoot_catch="R")
assert shoot_catch.to_query() == "shootsCatches=R"
Loading

0 comments on commit fd91cfd

Please sign in to comment.