diff --git a/README.md b/README.md index b3d1a61..4494e6b 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,17 @@ This is a Python library for interacting with the DataGolf APIs. DataGolf is a g This is being built to support some ML projects I am working on. I will be continuing to add more endpoints as I need them. If you have a specific endpoint you need, please open a ticket for submit a PR. +--- +## Developer Note: +This is in development. Code structure will change as I get +all the endpoints added. So expect some renaming and convention changes. +### Contact +Im available on [Bluesky](https://bsky.app/profile/coreyjs.dev) for any questions or just general chats about enhancements. --- -# Usage -(Currently this only supports JSON formats, CSV is in progress) +# Usage + Installation & Setup +(Currently this only supports JSON formats, CSV is on the roadmap) ```python pip install data_golf @@ -33,16 +39,303 @@ client = DataGolfClient(api_key="YOUR_API_KEY") client = DataGolfClient(api_key="YOUR_API_KEY", verbose=True) ``` +--- + +# Main Modules + +These modules map directly to the [DataGolf API Documentation](https://datagolf.com/api-access) available on their site: + +1. General +2. Predictions +3. Live Predictions + +The Data Golf API is a paid service via [DataGolf.com](https://datagolf.com/api-access), there they will provide you with an API Key. This library is +only a helper utility, to make interacting and consuming the API easier. +--- # General APIs +### Player List + +
+ API Endpoint Info + +**Endpoint:** `/get-player-list` +**Method:** GET +**Formats:** JSON + +
+ ```python -# Player List players = client.general.player_list() +``` + + +### Current Season Tour Schedule + +
+ API Endpoint Info + +**Endpoint:** `/get-schedule` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|-------|--------|---------------------------------------------------------------------------| +| tour | str | all, pga, euro, kft, alt, liv | -# Current Season Tour Schedule +
+ +```python # Can use optinal parameter 'tour' to filter by tour: pga, euro, kft, alt, liv tour_schedule = client.general.tour_schedule() tour_schedule = client.general.tour_schedule(tour="pga") tour_schedule = client.general.tour_schedule(tour="liv") -``` \ No newline at end of file +``` + +### Field Updates + +
+ API Endpoint Info + +**Endpoint:** `/field-updates` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|-------|--------|---------------------------------------------------------------------------| +| tour | str | all, pga, euro, kft, alt, liv | + +
+ +```python +# tour = pga (default), euro, kft, opp, alt +rsp = client.general.field_updates() # defaults to pga +rsp = client.general.field_updates(tour="kft") +``` + +--- + +# Model Prediction APIs + +## Rankings + +
+ API Endpoint Info + + +**Endpoint:** `/preds/get-dg-rankings` +**Method:** GET +**Formats:** JSON + +
+ +```python +rankings = client.predictions.rankings() +``` + +## Pre Tournament Predictions + +
+ API Endpoint Info + +**Endpoint:** `/preds/pre-tournament` +**Method:** GET +**Formats:** JSON + +
+ +```python +rsp = client.predictions.pre_tournament() + +rsp = client.predictions.pre_tournament( + tour='pga', + dead_heat=True, + odds_format='american' +) +``` + +## Pre Tournament Prediction Archive + +
+ API Endpoint Info + +**Endpoint:** `/preds/pre-tournament-archive` +**Method:** GET +**Formats:** JSON + +
+ +```python +# Supports optional parameters event_id:, year:, odds_format: +rsp = client.predictions.pre_tournament_pred_archive() + +rsp = client.predictions.pre_tournament_pred_archive( + year=2021, +) +``` + +## Player Skill Decomposition + +
+ API Endpoint Info + +**Endpoint:** `/preds/player-decompositions` +**Method:** GET +**Formats:** JSON + +
+ +```python +# Supports optional parameters tour: +rsp = client.predictions.player_skill_decompositions() +rsp = client.predictions.player_skill_decompositions(tour='alt') +``` + +## Player Skill Ratings + +
+ API Endpoint Info + +**Endpoint:** `/preds/skill-ratings` +**Method:** GET +**Formats:** JSON + +
+ +```python +# Supports optional param display: (value, rank) +rsp = client.predictions.player_skill_ratings() +rsp = client.predictions.player_skill_ratings(display="rank") +``` + +## Detailed Approach Skill + +
+ API Endpoint Info + +**Endpoint:** `/preds/approach-skill` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|--------|--------|---------------------------------------------------------------------------| +| period | str | l24 (last 24 months) (default),
l12 (last 12 months), ytd (year to date) | + +
+ +```python +rsp = client.predictions.detailed_approach_skill() +rsp = client.predictions.detailed_approach_skill(period='ytd') +``` + +## Fantasy Projections +
+ API Endpoint Info + + +**Endpoint:** `/preds/fantasy-projection-defaults` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|-------|-----|---------------------------------------------------------------| +| tour | str | pga (default), euro, opp (opposite field PGA TOUR event), alt | +| site | str | draftkings (default), fanduel, yahoo | +| slate | str | main (default), showdown, showdown_late, weekend, captain | + +
+ +```python +rsp = client.predictions.fantasy_projection() +rsp = client.predictions.fantasy_projection(tour='pga', site='fanduel', slate='showdown') +``` + +--- + +# Live Predictions + + +### Live Model Predictions + +
+ API Endpoint Info + + +**Endpoint:** `/preds/in-play` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|-------------|------|---------------------------------------------------------------| +| tour | str | pga (default), euro, opp (opposite field PGA TOUR event), alt | +| dead_head | bool | False (default), True | +| odds_format | str | percent (default), american, decimal, fraction | + +
+ +```python +data = dg.live_predictions.live_in_play() +data = dg.live_predictions.live_in_play(tour='kft', odds_format='american') +``` + + +### Live Tournament Stats + +Returns live strokes-gained and traditional stats for every player during PGA Tour tournaments. + + +
+ API Endpoint Info + + +**Endpoint:** `/preds/live-tournament-stats` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|---------|---------|---------------------------------------------------------------| +| stats | csv str | Comma-separated list of statistics to be returned. Supports: sg_putt, sg_arg, sg_app, sg_ott, sg_t2g, sg_bs, sg_total,
distance, accuracy, gir, prox_fw, prox_rgh, scrambling | +| round | str | event_avg, 1, 2, 3, 4 | +| display | str | value (default), rank | + +
+ +```python +data = dg.live_predictions.live_tournament_stats() +data = dg.live_predictions.live_tournament_stats(stats="sq_arg, sg_bs", disppaly="rank") +``` + + +### Live Hole Scoring Distruibution + +Returns live hole scoring averages and distrubutions (birdies, pars, bogeys, etc.) broken down by tee time wave. + + +
+ API Endpoint Info + + +**Endpoint:** `/preds/live-hole-stats` +**Method:** GET +**Formats:** JSON + +| Param | Type | Ex | +|---------|------|--------------------------------------------------------------| +| tour | str | pga (default), euro, opp (opposite field PGA TOUR event), kft, alt | +| round | str | event_avg, 1, 2, 3, 4 | +| display | str | value (default), rank | + +
+ +```python +data = dg.live_predictions.live_hole_stats() +data = dg.live_predictions.live_hole_stats(tour='kft') +``` + + + + + + + diff --git a/data_golf/api/general.py b/data_golf/api/general.py index 13a8c7a..25fbe7f 100644 --- a/data_golf/api/general.py +++ b/data_golf/api/general.py @@ -28,6 +28,7 @@ def field_updates(self, tour: str = "pga", f_format: str = "json") -> List[dict] Up-to-the-minute field updates on WDs, Monday Qualifiers, tee times, and fantasy salaries for PGA Tour, European Tour, and Korn Ferry Tour events. Includes data golf IDs and tour-specific IDs for each player in the field. + :type tour: str := pga (default), euro, kft, opp, alt :return: """ diff --git a/data_golf/api/live_prediction.py b/data_golf/api/live_prediction.py new file mode 100644 index 0000000..14ed369 --- /dev/null +++ b/data_golf/api/live_prediction.py @@ -0,0 +1,74 @@ +class LivePrediction: + def __init__(self, client): + self.client = client + self._path = "/preds" + + def live_in_play( + self, + tour: str = "pga", + dead_heat: bool = False, + odds_format: str = "percent", + f_format: str = "json", + ): + """ + Returns live (updating at 5 minute intervals) finish probabilities for ongoing PGA and European Tour tournaments. + :param tour: pga (default), euro, opp (opposite field PGA TOUR event), kft, alt + :param dead_heat: False (default), True + :param odds_format: percent (default), american, decimal, fraction + :param f_format: Defaults to JSON. + :return: + """ + query_p = { + "odds_format": odds_format, + "dead_heat": "yes" if dead_heat else "no", + "tour": tour, + } + + return self.client.get( + resource=f"{self._path}/in-play", params=query_p, format=f_format + ) + + def live_tournament_stats( + self, + stats: str = None, + round: str = None, + display: str = "value", + f_format: str = "json", + ): + """ + Returns live strokes-gained and traditional stats for every player during + PGA Tour tournaments + :param stats: Comma-seperated list of statistics to be returned. Accepts: + [sg_putt, sg_arg, sg_app, sg_ott, sg_t2g, sg_bs, sg_total, distance, + accuracy, gir, prox_fw, prox_rgh, scrambling] + :param round: Specifies the round: Accepts: (event_avg, 1, 2, 3, 4) + :param display: Specifies how stats are displayed. Accepts values: value (default), rank + :param f_format: + :return: + """ + query_p = { + "display": display, + } + + if stats: + query_p["stats"] = stats + if round: + query_p["round"] = round + return self.client.get( + resource=f"{self._path}/live-tournament-stats", + params=query_p, + format=f_format, + ) + + def live_hole_stats(self, tour: str = "pga", f_format: str = "json"): + """ + Returns live hole scoring averages and distrubutions (birdies, pars, bogeys, etc.) broken down by tee time wave. + :param tour: pga (default), euro, kft, opp, alt + :param f_format: Defaults to JSON. + :return: + """ + return self.client.get( + resource=f"{self._path}/live-hole-stats", + params={"tour": tour}, + format=f_format, + ) diff --git a/data_golf/api/model.py b/data_golf/api/prediction.py similarity index 95% rename from data_golf/api/model.py rename to data_golf/api/prediction.py index 806c611..31eceb4 100644 --- a/data_golf/api/model.py +++ b/data_golf/api/prediction.py @@ -1,4 +1,4 @@ -class Model: +class Prediction: def __init__(self, client): self.client = client self._path = "/preds" @@ -12,7 +12,7 @@ def rankings(self, f_format: str = "json") -> dict: resource=f"{self._path}/get-dg-rankings", format=f_format ) - def pre_tournament_pred( + def pre_tournament( self, tour: str = "pga", add_position: str = None, @@ -44,14 +44,14 @@ def pre_tournament_pred_archive( self, event_id: str = None, year: str = None, - odd_format: str = "percent", + odds_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 odds_format: percent (default), american, decimal, fraction :param f_format: json (default) :return: dict """ @@ -62,7 +62,7 @@ def pre_tournament_pred_archive( query_p["event_id"] = event_id if year: query_p["year"] = year - query_p["odds_format"] = odd_format + query_p["odds_format"] = odds_format query_p["file_format"] = f_format return self.client.get( diff --git a/data_golf/client.py b/data_golf/client.py index d8ba7de..77aa943 100644 --- a/data_golf/client.py +++ b/data_golf/client.py @@ -1,7 +1,8 @@ -from data_golf.api.model import Model +from data_golf.api.prediction import Prediction from data_golf.config import DGConfig -from data_golf.http import HttpClient +from data_golf.http_client import HttpClient from data_golf.api.general import General +from data_golf.api.live_prediction import LivePrediction class DGCInvalidApiKey(Exception): @@ -25,7 +26,8 @@ def __init__( # Endpoints self.general = General(self._http_client) - self.model = Model(self._http_client) + self.predictions = Prediction(self._http_client) + self.live_predictions = LivePrediction(self._http_client) def _validate_api_key(self, api_key: str) -> None: """ diff --git a/data_golf/http.py b/data_golf/http_client.py similarity index 86% rename from data_golf/http.py rename to data_golf/http_client.py index 4a80570..d151aef 100644 --- a/data_golf/http.py +++ b/data_golf/http_client.py @@ -6,6 +6,14 @@ import logging +class DGForbidden(Exception): + pass + + +class DGBadRequest(Exception): + pass + + class HttpClient: def __init__(self, config) -> None: self._config = config @@ -51,6 +59,12 @@ def get( **kwargs, ) + if r.status_code == 403: + raise DGForbidden("403 Forbidden: Check your API key.") + + if r.status_code == 400: + raise DGBadRequest(r.content) + if self._config.verbose: logging.info(f"API URL: {r.url}") logging.info(kwargs["headers"]) diff --git a/poetry.lock b/poetry.lock index d30407f..df7a167 100644 --- a/poetry.lock +++ b/poetry.lock @@ -256,13 +256,13 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -408,4 +408,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "e69e37072721d59a36230202707fcf652ecc3433720b517f7074980d5be089b9" +content-hash = "99248cc0f1a31b4da2e604f9c2b2a709c68ecec2cb21481044be4ecb26a94a4c" diff --git a/pyproject.toml b/pyproject.toml index 1fd8c1b..0d2ad6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,30 @@ [tool.poetry] name = "data_golf" -version = "0.3.0" -description = "API Wrapper for Data golf endpoints" +version = "0.4.0" +description = "API Wrapper for DataGolf.com endpoints." authors = ["Corey Schaf "] readme = "README.md" +packages = [{include = "data_golf"}] +license = "MIT" +homepage = "https://github.com/coreyjs/data-golf-api" +repository = "https://github.com/coreyjs/data-golf-api" +keywords = ["golf", "datagolf", "data golf", "golf data", "golf AI", "golf machine learning", "golf ML", "golf stats"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules" +] [tool.poetry.dependencies] python = "^3.9" -httpx = "*" +httpx = "^0.27.0" [tool.poetry.group.dev.dependencies] pytest="^7.1.3" @@ -19,3 +36,29 @@ black = "*" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +line-length = 121 \ No newline at end of file diff --git a/tests/api/test_model.py b/tests/api/test_prediction.py similarity index 88% rename from tests/api/test_model.py rename to tests/api/test_prediction.py index 6748aa4..6af60a3 100644 --- a/tests/api/test_model.py +++ b/tests/api/test_prediction.py @@ -3,7 +3,7 @@ @mock.patch("httpx.Client.get") def test_rankings(d_m, dg_client): - dg_client.model.rankings() + dg_client.predictions.rankings() d_m.assert_called_once() assert ( "https://feeds.datagolf.com/preds/get-dg-rankings?" in d_m.call_args[1]["url"] @@ -14,7 +14,7 @@ def test_rankings(d_m, dg_client): @mock.patch("httpx.Client.get") def test_pre_tournament_pred(d_m, dg_client): - dg_client.model.pre_tournament_pred() + dg_client.predictions.pre_tournament() d_m.assert_called_once() assert "https://feeds.datagolf.com/preds/pre-tournament?" in d_m.call_args[1]["url"] @@ -26,7 +26,7 @@ def test_pre_tournament_pred(d_m, dg_client): @mock.patch("httpx.Client.get") def test_pre_tournament_with_params(d_m, dg_client): - dg_client.model.pre_tournament_pred( + dg_client.predictions.pre_tournament( tour="euro", add_position="1,2,3", dead_heat=False, odds_format="american" ) d_m.assert_called_once() @@ -41,7 +41,7 @@ def test_pre_tournament_with_params(d_m, dg_client): @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") + dg_client.predictions.pre_tournament_pred_archive(event_id="100") d_m.assert_called_once() assert ( "https://feeds.datagolf.com/preds/pre-tournament-archive?" @@ -55,7 +55,7 @@ def test_pre_tournament_pred_archive(d_m, dg_client): @mock.patch("httpx.Client.get") def test_player_skill_decomp(d_m, dg_client): - dg_client.model.player_skill_decompositions(tour="alt") + dg_client.predictions.player_skill_decompositions(tour="alt") d_m.assert_called_once() assert ( "https://feeds.datagolf.com/preds/player-decompositions?" @@ -68,7 +68,7 @@ def test_player_skill_decomp(d_m, dg_client): @mock.patch("httpx.Client.get") def test_player_skill_ratings(d_m, dg_client): - dg_client.model.player_skill_ratings() + dg_client.predictions.player_skill_ratings() d_m.assert_called_once() assert "https://feeds.datagolf.com/preds/skill-ratings?" in d_m.call_args[1]["url"] @@ -78,7 +78,7 @@ def test_player_skill_ratings(d_m, dg_client): @mock.patch("httpx.Client.get") def test_detailed_approach_skill(d_m, dg_client): - dg_client.model.detailed_approach_skill() + dg_client.predictions.detailed_approach_skill() d_m.assert_called_once() assert "https://feeds.datagolf.com/preds/approach-skill?" in d_m.call_args[1]["url"] diff --git a/tests/conftest.py b/tests/conftest.py index 0f0796d..3ab7c36 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,6 @@ import pytest -from data_golf.client import DataGolfClient +from data_golf import DataGolfClient @pytest.fixture(scope="function")