Skip to content

Commit 9df7a61

Browse files
committed
Feature DataGolf API Client: Unoffical.
Adds endpoints for general information at this time. Adds base structure for handling http requests, parsing, api_key handling. Adds Pytest, black and ruff checks to CI/CD pipleline.
1 parent 7d7c327 commit 9df7a61

16 files changed

Lines changed: 765 additions & 0 deletions

.github/workflows/python-app.yml

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# This workflow will install Python dependencies, run tests and lint with a single version of Python
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3+
4+
name: NHL-API-PY
5+
6+
on:
7+
push:
8+
branches: [ "main" ]
9+
pull_request:
10+
branches: [ "main" ]
11+
12+
permissions:
13+
contents: read
14+
15+
jobs:
16+
test:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
# If you wanted to use multiple Python versions, you'd have specify a matrix in the job and
22+
# reference the matrixe python version here.
23+
- uses: actions/setup-python@v5
24+
with:
25+
python-version: 3.9.18
26+
27+
# Cache the installation of Poetry itself, e.g. the next step. This prevents the workflow
28+
# from installing Poetry every time, which can be slow. Note the use of the Poetry version
29+
# number in the cache key, and the "-0" suffix: this allows you to invalidate the cache
30+
# manually if/when you want to upgrade Poetry, or if something goes wrong. This could be
31+
# mildly cleaner by using an environment variable, but I don't really care.
32+
- name: cache poetry install
33+
uses: actions/cache@v4
34+
with:
35+
path: ~/.local
36+
key: poetry-1.5.1-0
37+
38+
# Install Poetry. You could do this manually, or there are several actions that do this.
39+
# `snok/install-poetry` seems to be minimal yet complete, and really just calls out to
40+
# Poetry's default install script, which feels correct. I pin the Poetry version here
41+
# because Poetry does occasionally change APIs between versions and I don't want my
42+
# actions to break if it does.
43+
#
44+
# The key configuration value here is `virtualenvs-in-project: true`: this creates the
45+
# venv as a `.venv` in your testing directory, which allows the next step to easily
46+
# cache it.
47+
- uses: snok/install-poetry@v1
48+
with:
49+
version: 1.5.1
50+
virtualenvs-create: true
51+
virtualenvs-in-project: true
52+
53+
# Cache your dependencies (i.e. all the stuff in your `pyproject.toml`). Note the cache
54+
# key: if you're using multiple Python versions, or multiple OSes, you'd need to include
55+
# them in the cache key. I'm not, so it can be simple and just depend on the poetry.lock.
56+
- name: cache deps
57+
id: cache-deps
58+
uses: actions/cache@v3
59+
with:
60+
path: .venv
61+
key: pydeps-${{ hashFiles('**/poetry.lock') }}
62+
63+
# Install dependencies. `--no-root` means "install all dependencies but not the project
64+
# itself", which is what you want to avoid caching _your_ code. The `if` statement
65+
# ensures this only runs on a cache miss.
66+
- run: poetry install --no-interaction --no-root
67+
if: steps.cache-deps.outputs.cache-hit != 'true'
68+
69+
# Now install _your_ project. This isn't necessary for many types of projects -- particularly
70+
# things like Django apps don't need this. But it's a good idea since it fully-exercises the
71+
# pyproject.toml and makes that if you add things like console-scripts at some point that
72+
# they'll be installed and working.
73+
- run: poetry install --no-interaction
74+
75+
# And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml`
76+
# so this line is super-simple. But it could be as complex as you need.
77+
- run: poetry run pytest
78+
79+
# run a check for black
80+
- name: poetry run black . --check
81+
run: poetry run black . --check
82+
83+
# run a lint check with ruff
84+
- name: poetry run ruff check .
85+
run: poetry run ruff check .

.gitignore

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.idea
2+
.ipynb_checkpoints
3+
.mypy_cache
4+
.vscode
5+
__pycache__
6+
.pytest_cache
7+
htmlcov
8+
dist
9+
site
10+
.coverage
11+
coverage.xml
12+
.netlify
13+
test.db
14+
log.txt
15+
Pipfile.lock
16+
env3.*
17+
env
18+
docs_build
19+
venv
20+
docs.zip
21+
archive.zip
22+
23+
# vim temporary files
24+
*~
25+
.*.sw?
26+
27+
*/.DS_Store

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.9.18

data_golf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .client import DataGolfClient # noqa: F401

data_golf/api/__init__.py

Whitespace-only changes.

data_golf/api/general.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from typing import List
2+
3+
4+
class General:
5+
def __init__(self, client):
6+
self.client = client
7+
8+
def player_list(self, format: str = "json") -> List[dict]:
9+
"""
10+
11+
:return:
12+
"""
13+
return self.client.get(resource="/get-player-list", format=format)
14+
15+
def tour_schedule(self, format: str = "json") -> List[dict]:
16+
"""
17+
18+
:return:
19+
"""
20+
return self.client.get(resource="/get-schedule", format=format)

data_golf/client.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from data_golf.config import DGConfig
2+
from data_golf.http import HttpClient
3+
from data_golf.api.general import General
4+
5+
6+
class DGCInvalidApiKey(Exception):
7+
pass
8+
9+
10+
class DataGolfClient:
11+
def __init__(
12+
self,
13+
api_key: str,
14+
verbose: bool = False,
15+
timeout: int = 15,
16+
ssl_verify: bool = True,
17+
) -> None:
18+
self._validate_api_key(api_key)
19+
20+
self._config = DGConfig(
21+
api_key=api_key, verbose=verbose, timeout=timeout, ssl_verify=ssl_verify
22+
)
23+
self._http_client = HttpClient(self._config)
24+
25+
# Endpoints
26+
self.general = General(self._http_client)
27+
28+
def _validate_api_key(self, api_key: str) -> None:
29+
"""
30+
Private method to validate the API key.
31+
:param api_key:
32+
:return:
33+
"""
34+
if not isinstance(api_key, str):
35+
raise DGCInvalidApiKey("API key must be a string.")
36+
if not api_key:
37+
raise DGCInvalidApiKey("API key cannot be empty.")

data_golf/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
class DGConfig:
2+
def __init__(
3+
self,
4+
api_key: str,
5+
verbose: bool = False,
6+
timeout: int = 15,
7+
ssl_verify: bool = True,
8+
) -> None:
9+
self.api_key = api_key
10+
self.verbose = verbose
11+
self.timeout = timeout
12+
self.ssl_verify = ssl_verify
13+
self.base_url = "https://feeds.datagolf.com"

data_golf/http.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from data_golf.request_helpers import RequestHelpers
2+
3+
import httpx
4+
import logging
5+
6+
7+
class HttpClient:
8+
def __init__(self, config) -> None:
9+
self._config = config
10+
if self._config.verbose:
11+
logging.basicConfig(level=logging.INFO)
12+
13+
def _build_url(self, resource: str, format: str):
14+
"""
15+
Private method to build the URL for the Data Golf API.
16+
:param resource:
17+
:param format:
18+
:return:
19+
"""
20+
params = [f"key={self._config.api_key}", f"file_format={format}"]
21+
url = ""
22+
23+
if len(resource.split("?")) > 1:
24+
url = f"{self._config.base_url}{resource}&{'&'.join(params)}"
25+
else:
26+
url = f"{self._config.base_url}{resource}?{'&'.join(params)}"
27+
return url
28+
29+
@RequestHelpers.prepare_request
30+
def get(self, resource: str, format: str = "json", **kwargs) -> httpx.request:
31+
"""
32+
Private method to make a get request to the Data Golf API. This wraps the lib httpx functionality.
33+
:param format:
34+
:param resource:
35+
:return:
36+
"""
37+
with httpx.Client(
38+
verify=self._config.ssl_verify, timeout=self._config.timeout
39+
) as client:
40+
r: httpx.request = client.get(
41+
url=self._build_url(resource, format), **kwargs
42+
)
43+
44+
if self._config.verbose:
45+
logging.info(f"API URL: {r.url}")
46+
logging.info(kwargs["headers"])
47+
48+
return r.json()

data_golf/request_helpers.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from functools import wraps
2+
3+
ALLOWED_FORMATS = ["json"]
4+
5+
6+
class RequestHelpers:
7+
8+
@classmethod
9+
def _set_headers(cls, f_format) -> dict[str, str]:
10+
headers = {}
11+
if f_format not in ALLOWED_FORMATS:
12+
raise ValueError("format must be 'json'. CSV support is coming")
13+
14+
if f_format == "json":
15+
headers["Content-Type"] = "application/json"
16+
elif f_format == "csv":
17+
headers["Content-Type"] = "text/csv; charset=utf-8"
18+
19+
return headers
20+
21+
@staticmethod
22+
def prepare_request(func):
23+
@wraps(func)
24+
def wrapper(*args, **kwargs):
25+
f_format = kwargs.get("format", "json")
26+
27+
headers = RequestHelpers._set_headers(f_format=f_format)
28+
29+
kwargs["headers"] = {**kwargs.get("headers", {}), **headers}
30+
return func(*args, **kwargs)
31+
32+
return wrapper

0 commit comments

Comments
 (0)