Skip to content

Commit

Permalink
Feature - Model Prediction Endpoints
Browse files Browse the repository at this point in the history
Adds model.pre_tournament_pred_archive, model.player_skill_decompositions, model.player_skill_ratings, model.detailed_appraoch_skill, model.fantasy_projection.

Change: Changes how the query_parameters are passed to the HTTP client and helpers, this is no longer a big ugly string, but a dict.  Adds helpers in http.py to build final query parameter
  • Loading branch information
coreyjs committed Jun 11, 2024
1 parent 544cfd9 commit 922a54c
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 48 deletions.
2 changes: 1 addition & 1 deletion data_golf/api/general.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def tour_schedule(self, tour: str = "all", f_format: str = "json") -> dict:
"""
Current season schedule for PGA Tour, European Tour, Korn Ferry Tour, and LIV.
:param tour: str optional defaults to 'all', the tour you want the schedule for. values: all, pga, euro, kft, alt, liv
:param format:
:param f_format:
:return:
"""
return self.client.get(resource=f"/get-schedule?tour={tour}", format=f_format)
Expand Down
109 changes: 103 additions & 6 deletions data_golf/api/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,109 @@ def pre_tournament_pred(
:param f_format: json (default)
:return:
"""
# i think there is a better way to handle building and appending query params.
q_p = f"tour={tour}"
q_p += f"&add_position={add_position}" if add_position else ""
q_p += "&dead_heat=yes" if dead_heat else "&dead_heat=no"
q_p += f"&odds_format={odds_format}"
query_p = {"tour": tour}
if add_position:
query_p["add_position"] = add_position

query_p["dead_heat"] = "yes" if dead_heat else "no"
query_p["odds_format"] = odds_format

return self.client.get(
resource=f"{self._path}/pre-tournament?", params=query_p, format=f_format
)

def pre_tournament_pred_archive(
self,
event_id: str = None,
year: str = None,
odd_format: str = "percent",
f_format="json",
) -> dict:
"""
Returns pre-tournament predictions for a specific event or year.
:param event_id: The event id for the tournament.
:param year: The year for the tournament.
:param odd_format: percent (default), american, decimal, fraction
:param f_format: json (default)
:return: dict
"""

query_p = {}

if event_id:
query_p["event_id"] = event_id
if year:
query_p["year"] = year
query_p["odds_format"] = odd_format
query_p["file_format"] = f_format

return self.client.get(
resource=f"{self._path}/pre-tournament-archive",
params=query_p,
format=f_format,
)

def player_skill_decompositions(
self, tour: str = "pga", f_format: str = "json"
) -> dict:
"""
Returns player skill decompositions for a specific tour.
:param tour: pga, euro, kft, opp, alt
:param f_format: json (default)
:return: dict
"""
query_p = {"tour": tour}
return self.client.get(
resource=f"{self._path}/player-decompositions",
params=query_p,
format=f_format,
)

def player_skill_ratings(
self, display: str = "value", f_format: str = "json"
) -> dict:
"""
Returns our estimate and rank for each skill for all players with sufficient Shotlink measured rounds (at least 30 rounds in the last year or 50 in the last 2 years).
:param display: value, rank
:param f_format: json (default)
:return: dict
"""
query_p = {"display": display}
return self.client.get(
resource=f"{self._path}/pre-tournament?{q_p}", format=f_format
resource=f"{self._path}/skill-ratings",
params=query_p,
format=f_format,
)

def detailed_approach_skill(
self, period: str = "l24", f_format: str = "json"
) -> dict:
"""
Returns detailed player-level approach performance stats (strokes-gained per shot, proximity, GIR, good shot rate, poor shot avoidance rate) across various yardage/lie buckets.
:param period: l24 (last 24 months) (default), l12 (last 12 months), ytd (year to date)
:param f_format: json (default)
:return: dict
"""
query_p = {"period": period}
return self.client.get(
resource=f"{self._path}/approach-skill",
params=query_p,
format=f_format,
)

def fantasy_projection(self, tour: str = "pga", slate: str = "main", site: str = "draftkings", f_format: str = "json") -> dict:
"""
Returns our fantasy projections for a specific tour and slate.
:param tour: pga (default), euro, opp (opposite field PGA TOUR event), alt
:param slate: main (default), showdown, showdown_late, weekend, captain
:param site: draftkings (default), fanduel, yahoo
:param f_format: json (default)
:return: dict
"""
query_p = {"tour": tour, "slate": slate, "site": site}
return self.client.get(
resource=f"{self._path}/fantasy-projection-defaults",
params=query_p,
format=f_format,
)

38 changes: 29 additions & 9 deletions data_golf/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,41 @@ def __init__(self, config) -> None:
if self._config.verbose:
logging.basicConfig(level=logging.INFO)

def _build_url(self, resource: str, format: str):
def _build_url(self, resource: str, query_params: dict, format: str):
"""
Private method to build the URL for the Data Golf API.
:param resource:
:param format:
:return:
"""
params = [f"key={self._config.api_key}", f"file_format={format}"]
url = ""
query_params["key"] = self._config.api_key
query_params["file_format"] = format

if len(resource.split("?")) > 1:
url = f"{self._config.base_url}{resource}&{'&'.join(params)}"
else:
url = f"{self._config.base_url}{resource}?{'&'.join(params)}"
url = f"{self._config.base_url}{resource}?"

url = self._append_query_params(query_params=query_params, url=url)

return url

def _append_query_params(self, query_params: dict, url: str) -> str:
"""
Private method to append query parameters to the URL.
:param query_params:
:param url:
:return:
"""
if query_params:
for k, v in query_params.items():
url += f"&{k}={v}"
return url

@RequestHelpers.prepare_request
def get(self, resource: str, format: str = "json", **kwargs) -> httpx.request:
def get(
self, resource: str, params: dict = None, format: str = "json", **kwargs
) -> httpx.request:
"""
Private method to make a get request to the Data Golf API. This wraps the lib httpx functionality.
:param params:
:param format:
:param resource:
:return:
Expand All @@ -38,7 +53,12 @@ def get(self, resource: str, format: str = "json", **kwargs) -> httpx.request:
verify=self._config.ssl_verify, timeout=self._config.timeout
) as client:
r: httpx.request = client.get(
url=self._build_url(resource, format), **kwargs
url=self._build_url(
resource=resource,
query_params=params if params else {},
format=format,
),
**kwargs,
)

if self._config.verbose:
Expand Down
39 changes: 15 additions & 24 deletions tests/api/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,7 @@ def test_request_gets_json_header(d_m, dg_client):
def test_api_key_appends_to_request(d_m, dg_client):
dg_client.general.player_list()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/get-player-list?key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/get-player-list?" in d_m.call_args[1]["url"]
assert "key=test_key" in d_m.call_args[1]["url"]


Expand All @@ -30,47 +27,41 @@ def test_request_will_err_on_csv_format(dg_client):
def test_player_list(d_m, dg_client):
dg_client.general.player_list()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/get-player-list?key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/get-player-list?" in d_m.call_args[1]["url"]
assert "key=test_key" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_tour_schedule(d_m, dg_client):
dg_client.general.tour_schedule()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/get-schedule?tour=all&key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/get-schedule?" in d_m.call_args[1]["url"]
assert "tour=all" in d_m.call_args[1]["url"]
assert "key=test_key" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_tour_schedule_for_tour(d_m, dg_client):
dg_client.general.tour_schedule(tour="kft")
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/get-schedule?tour=kft&key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/get-schedule?" in d_m.call_args[1]["url"]
assert "tour=kft" in d_m.call_args[1]["url"]
assert "key=test_key" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_field_updates(d_m, dg_client):
dg_client.general.field_updates()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/field-updates?key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/field-updates?" in d_m.call_args[1]["url"]
assert "key=test_key" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_field_updates_with_tour_euro(d_m, dg_client):
dg_client.general.field_updates(tour="euro")
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/field-updates?tour=euro&key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/field-updates?" in d_m.call_args[1]["url"]

assert "key=test_key" in d_m.call_args[1]["url"]
assert "tour=euro" in d_m.call_args[1]["url"]
72 changes: 64 additions & 8 deletions tests/api/test_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,22 @@ def test_rankings(d_m, dg_client):
dg_client.model.rankings()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/preds/get-dg-rankings?key=test_key&file_format=json"
"https://feeds.datagolf.com/preds/get-dg-rankings?" in d_m.call_args[1]["url"]
)

assert "key=test_key" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_pre_tournament_pred(d_m, dg_client):
dg_client.model.pre_tournament_pred()
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/preds/pre-tournament?tour=pga&dead_heat=yes&odds_format=percent&key=test_key&file_format=json"
)
assert "https://feeds.datagolf.com/preds/pre-tournament?" in d_m.call_args[1]["url"]

assert "key=test_key" in d_m.call_args[1]["url"]
assert "tour=pga" in d_m.call_args[1]["url"]
assert "odds_format=percent" in d_m.call_args[1]["url"]
assert "dead_heat=yes" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
Expand All @@ -27,7 +30,60 @@ def test_pre_tournament_with_params(d_m, dg_client):
tour="euro", add_position="1,2,3", dead_heat=False, odds_format="american"
)
d_m.assert_called_once()
assert "https://feeds.datagolf.com/preds/pre-tournament?" in d_m.call_args[1]["url"]

assert "key=test_key" in d_m.call_args[1]["url"]
assert "tour=euro" in d_m.call_args[1]["url"]
assert "add_position=1,2,3" in d_m.call_args[1]["url"]
assert "dead_heat=no" in d_m.call_args[1]["url"]
assert "odds_format=american" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_pre_tournament_pred_archive(d_m, dg_client):
dg_client.model.pre_tournament_pred_archive(event_id="100")
d_m.assert_called_once()
assert (
d_m.call_args[1]["url"]
== "https://feeds.datagolf.com/preds/pre-tournament?tour=euro&add_position=1,2,3&dead_heat=no&odds_format=american&key=test_key&file_format=json"
"https://feeds.datagolf.com/preds/pre-tournament-archive?"
in d_m.call_args[1]["url"]
)

assert "key=test_key" in d_m.call_args[1]["url"]
assert "odds_format=percent" in d_m.call_args[1]["url"]
assert "event_id=100" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_player_skill_decomp(d_m, dg_client):
dg_client.model.player_skill_decompositions(tour="alt")
d_m.assert_called_once()
assert (
"https://feeds.datagolf.com/preds/player-decompositions?"
in d_m.call_args[1]["url"]
)

assert "key=test_key" in d_m.call_args[1]["url"]
assert "tour=alt" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_player_skill_ratings(d_m, dg_client):
dg_client.model.player_skill_ratings()
d_m.assert_called_once()
assert (
"https://feeds.datagolf.com/preds/skill-ratings?"
in d_m.call_args[1]["url"]
)

assert "key=test_key" in d_m.call_args[1]["url"]
assert "display=value" in d_m.call_args[1]["url"]


@mock.patch("httpx.Client.get")
def test_detailed_approach_skill(d_m, dg_client):
dg_client.model.detailed_approach_skill()
d_m.assert_called_once()
assert "https://feeds.datagolf.com/preds/approach-skill?" in d_m.call_args[1]["url"]

assert "key=test_key" in d_m.call_args[1]["url"]
assert "period=l24" in d_m.call_args[1]["url"]

0 comments on commit 922a54c

Please sign in to comment.