├── AUTHORS.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── peyton ├── __init__.py ├── competition.py ├── silly.py ├── throne.py └── user.py ├── setup.cfg └── setup.py /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | Author 2 | ================= 3 | 4 | - Ross Taylor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Throne.AI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include peyton/* *.rst *.txt LICENSE -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Throne.AI 2 | 3 | Peyton: Throne.ai API 4 | =================================== 5 | 6 | **Peyton** is a Python library that allows you to interact with the Throne.ai platform for sports prediction. From this library, you can download historical and competition data, user ranking data, game statistics data, lineups data, and submit predictions. 7 | 8 | Installation 9 | ------------ 10 | 11 | Peyton is developed for Python 3.5 onwards. The recommended way to 12 | install peyton is via pip. 13 | 14 | ``` 15 | pip install peyton 16 | ``` 17 | 18 | The Hitchhiker's Guide to Python has details on how to install Python and pip. 19 | 20 | Getting Started 21 | ---------- 22 | 23 | Obtain your API token from your profile page (click your username in the top banner) under the **Edit Profile** tab. In Python, you can initialize an instance of *peyton* as follows: 24 | 25 | ```python 26 | import peyton 27 | throne = peyton.Throne(username='USERNAME', token="TOKEN") 28 | ``` 29 | 30 | With the Throne API instance you can then interact with Throne.ai: 31 | 32 | ```python 33 | # View your XP 34 | throne.user.get_xp() 35 | 36 | # View your performance 37 | throne.user.get_performance() 38 | 39 | # Plot your historical score in a competition 40 | throne.user.plot_score('English Premier League') 41 | 42 | # Plot your edge in each competition 43 | throne.user.plot_edge() 44 | 45 | # Get historical data for a competition 46 | throne.competition('NFL').get_historical_data() 47 | my_historical_data = throne.competition.historical_data 48 | 49 | # Get competition data for a competition 50 | throne.competition('NFL').get_competition_data() 51 | my_competition_data = throne.competition.competition_data 52 | 53 | # Get game statistics data for a competition 54 | throne.competition('NFL').get_game_statistics_data() 55 | my_game_statistics_data = throne.competition.game_statistics_data 56 | 57 | # Get lineups data for a competition 58 | throne.competition('NFL').get_lineups_data() 59 | my_lineups_data = throne.competition.lineups_data 60 | 61 | # Merging datasources 62 | import pandas as pd 63 | game_statistics_df = pd.merge(my_historical_data, my_game_statistics_data) 64 | lineups_df = pd.merge(my_historical_data, my_lineups_data) 65 | 66 | # Get players data 67 | players_data = throne.competition('NFL').get_players_data() 68 | 69 | # Submit predictions 70 | throne.competition('NFL').submit(my_submission_df) 71 | 72 | # Make an audible 73 | throne.omaha() 74 | ``` 75 | 76 | We will look to provide documentation for the API once we expand its capabilities. 77 | 78 | **Note : if you are looking to reuse data for development, then SAVE it rather than reloading from the API each time you run a script or Notebook. Otherwise you could end up locked out!** 79 | 80 | Discussion and Support 81 | --------------------------- 82 | 83 | Please consult our Slack Channel at https://throneai.slack.com. For an invite, go to your profile pack and click 'Slack Invite'. 84 | -------------------------------------------------------------------------------- /peyton/__init__.py: -------------------------------------------------------------------------------- 1 | from .competition import Competition 2 | from .throne import Throne 3 | from .user import User 4 | 5 | from . import stats -------------------------------------------------------------------------------- /peyton/competition.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import io 3 | import json 4 | import matplotlib.pyplot as plt 5 | import pandas as pd 6 | import requests 7 | import zipfile 8 | 9 | SUBMISSION_ROWS_TO_EXTRACT = ['id', 'team_1_prob', 'team_2_prob', 'team_tie_prob'] 10 | 11 | 12 | class UploadError(Exception): 13 | pass 14 | 15 | 16 | class AvailabilityError(Exception): 17 | pass 18 | 19 | 20 | class Competition(object): 21 | 22 | def __init__(self, api): 23 | """ 24 | Contains methods for retrieving competition based data 25 | """ 26 | self.api = api # Throne API instance with auth information 27 | 28 | def __call__(self, competition_name): 29 | """ 30 | When the user calls, we set the competition name 31 | """ 32 | 33 | if hasattr(self, 'competition'): 34 | if self.competition != competition_name: 35 | try: 36 | del self.historical_data 37 | except AttributeError: # historical data has not been fetched 38 | pass 39 | try: 40 | del self.competition_data 41 | except AttributeError: # competition data has not been fetched 42 | pass 43 | try: 44 | del self.game_statistics_data 45 | except AttributeError: # game statistics data has not been fetched 46 | pass 47 | try: 48 | del self.lineups_data 49 | except AttributeError: # lineups data has not been fetched 50 | pass 51 | try: 52 | del self.players_data 53 | except AttributeError: # players data has not been fetched 54 | pass 55 | 56 | self.competition = competition_name 57 | self.api.headers['X-COMPETITION-NAME'] = self.competition 58 | 59 | return self 60 | 61 | @property 62 | def available(self): 63 | """ 64 | Find available competitions 65 | lflflf 66 | Returns 67 | -------- 68 | - list of available competitions (name strs that you can query) 69 | """ 70 | 71 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/names/'), headers=self.api.headers) 72 | self.api._auth_report(result) 73 | 74 | return json.loads(result.content.decode("utf-8"))['result'] 75 | 76 | def get_historical_data(self, file_dir=None): 77 | """ 78 | Retrieves and sets historical data for the competition. 79 | 80 | Returns 81 | -------- 82 | self.historical_data - the competition's historical data 83 | """ 84 | 85 | if hasattr(self, 'historical_data'): 86 | return self.historical_data 87 | 88 | # user specified location 89 | if file_dir: 90 | self.historical_data = pd.read_csv(file_dir) 91 | return self.historical_data 92 | 93 | # If we reach this point, we're going through the API 94 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/historical/'), headers=self.api.headers) 95 | self.api._auth_report(result) 96 | 97 | # extract csv form zip and read into pandas DataFrame 98 | zp = zipfile.ZipFile(io.BytesIO(result.content)) 99 | 100 | file_names = [file.filename for file in zp.infolist() if '_historical_data' in file.filename] 101 | 102 | if not file_names: 103 | raise IndexError('No relevant data files detected') 104 | elif len(file_names) > 1: 105 | raise IndexError('More than one file detected - faulty endpoint') 106 | else: 107 | file_to_extract = file_names[0] 108 | 109 | self.historical_data = pd.read_csv(zp.open(file_to_extract)) 110 | 111 | return self.historical_data 112 | 113 | def get_game_statistics_data(self, file_dir=None): 114 | """ 115 | Retrieves and sets game statistics data for the competition. 116 | 117 | Returns 118 | -------- 119 | self.game_statistics_data - the competition's game statistics data 120 | """ 121 | 122 | if hasattr(self, 'game_statistics_data'): 123 | return self.game_statistics_data 124 | 125 | # user specified location 126 | if file_dir: 127 | self.game_statistics_data = pd.read_csv(file_dir) 128 | return self.game_statistics_data 129 | 130 | # If we reach this point, we're going through the API 131 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/gamestatistics/'), headers=self.api.headers) 132 | self.api._auth_report(result) 133 | 134 | # extract csv form zip and read into pandas DataFrame 135 | try: 136 | zp = zipfile.ZipFile(io.BytesIO(result.content)) 137 | except zipfile.BadZipFile: 138 | error_message = json.loads(str(result.content.decode('utf-8'))) 139 | raise AvailabilityError(error_message['message']) 140 | 141 | file_names = [file.filename for file in zp.infolist() if '_game_statistics_data' in file.filename] 142 | 143 | if not file_names: 144 | raise IndexError('No relevant data files detected') 145 | elif len(file_names) > 1: 146 | raise IndexError('More than one file detected - faulty endpoint') 147 | else: 148 | file_to_extract = file_names[0] 149 | 150 | self.game_statistics_data = pd.read_csv(zp.open(file_to_extract)) 151 | 152 | return self.game_statistics_data 153 | 154 | def get_lineups_data(self, file_dir=None): 155 | """ 156 | Retrieves and sets lineups data for the competition. 157 | 158 | Returns 159 | -------- 160 | self.lineups_data - the competition's lineups data 161 | """ 162 | 163 | if hasattr(self, 'lineups_data'): 164 | return self.lineups_data 165 | 166 | # user specified location 167 | if file_dir: 168 | self.lineups_data = pd.read_csv(file_dir) 169 | return self.lineups_data 170 | 171 | # If we reach this point, we're going through the API 172 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/lineups/'), headers=self.api.headers) 173 | self.api._auth_report(result) 174 | 175 | # extract csv form zip and read into pandas DataFrame 176 | try: 177 | zp = zipfile.ZipFile(io.BytesIO(result.content)) 178 | except zipfile.BadZipFile: 179 | error_message = json.loads(str(result.content.decode('utf-8'))) 180 | raise AvailabilityError(error_message['message']) 181 | 182 | file_names = [file.filename for file in zp.infolist() if '_lineups_data' in file.filename] 183 | 184 | if not file_names: 185 | raise IndexError('No relevant data files detected') 186 | elif len(file_names) > 1: 187 | raise IndexError('More than one file detected - faulty endpoint') 188 | else: 189 | file_to_extract = file_names[0] 190 | 191 | self.lineups_data = pd.read_csv(zp.open(file_to_extract)) 192 | 193 | return self.lineups_data 194 | 195 | def get_players_data(self): 196 | """ 197 | Retrieves player data for the competition 198 | 199 | Returns 200 | -------- 201 | self.players_data - the players data for the competition 202 | """ 203 | 204 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/players/'), headers=self.api.headers) 205 | self.api._auth_report(result) 206 | 207 | self.players_data = json.loads(result.content.decode("utf-8"))['result'] 208 | 209 | return self.players_data 210 | 211 | def get_competition_data(self, file_dir=None): 212 | """ 213 | Retrieves and sets upcoming data for the competition. 214 | 215 | Returns 216 | -------- 217 | self.competition_data - the competition's historical data 218 | """ 219 | 220 | if hasattr(self, 'competition_data'): 221 | return self.competition_data 222 | 223 | # user specified location 224 | if file_dir: 225 | self.competition_data = pd.read_csv(file_dir) 226 | return self.competition_data 227 | 228 | # If we reach this point, we're going through the API 229 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/data/upcoming/'), headers=self.api.headers) 230 | self.api._auth_report(result) 231 | 232 | # extract csv form zip and read into pandas DataFrame 233 | zp = zipfile.ZipFile(io.BytesIO(result.content)) 234 | 235 | file_names = [file.filename for file in zp.infolist() if '_competition_data' in file.filename] 236 | 237 | if not file_names: 238 | raise IndexError('No relevant data files detected') 239 | elif len(file_names) > 1: 240 | raise IndexError('More than one file detected - faulty endpoint') 241 | else: 242 | file_to_extract = file_names[0] 243 | 244 | self.competition_data = pd.read_csv(zp.open(file_to_extract)) 245 | 246 | return self.competition_data 247 | 248 | def submit(self, df): 249 | """ 250 | Submits predictions for upcoming games 251 | """ 252 | try: 253 | if 'confidence' in df.columns: 254 | if 'team_tie_prob' not in df.columns: 255 | final_df = df[['id', 'team_1_prob', 'team_2_prob', 'confidence']] 256 | else: 257 | final_df = df[SUBMISSION_ROWS_TO_EXTRACT + ['confidence']] 258 | else: 259 | if 'team_tie_prob' not in df.columns: 260 | final_df = df[['id', 'team_1_prob', 'team_2_prob']] 261 | else: 262 | final_df = df[SUBMISSION_ROWS_TO_EXTRACT] 263 | 264 | except KeyError: 265 | raise KeyError('Could not find probability (team_1_prob, team_2_prob) or ID columns (id) in your submitted DataFrame') 266 | 267 | final_df.index = [str(i) for i in final_df.index] # str format because of json parsing differences 268 | 269 | self.api.headers['X-PROB-SUBMISSION'] = json.dumps(final_df.to_dict()) 270 | 271 | result = requests.get('%s%s' % (self.api.BASE_URL, 'competition/submit/'), headers=self.api.headers) 272 | self.api._auth_report(result) 273 | 274 | content = json.loads(result.content.decode("utf-8"))['result'] 275 | 276 | if not content['success']: 277 | raise UploadError(content['error']) 278 | else: 279 | return content 280 | -------------------------------------------------------------------------------- /peyton/silly.py: -------------------------------------------------------------------------------- 1 | PLAY_DISTRIBUTION = { 2 | 0: "Handoff. Not much gain. [2 YARDS]", 3 | 1: "SACK! [-5 YARDS]", 4 | 2: "Short pass [3 YARDS]", 5 | 3: "FUMBLE [PATRIOTS TOUCHDOWN] :(", 6 | 4: "Medium pass [7 YARDS]", 7 | 5: "THROWS DEEP... [COLTS TOUCHDOWN] ^_^" 8 | } 9 | 10 | 11 | 12 | def random_play(x): 13 | """ 14 | Generates a play after an Omaha audible 15 | 16 | Parameters 17 | -------- 18 | x - int 19 | A "random" number generated by the football Gods 20 | 21 | Returns 22 | -------- 23 | str - commentary on the play 24 | """ 25 | return PLAY_DISTRIBUTION[x] 26 | 27 | 28 | -------------------------------------------------------------------------------- /peyton/throne.py: -------------------------------------------------------------------------------- 1 | import json 2 | import random 3 | import requests 4 | 5 | from .competition import Competition 6 | from .silly import random_play 7 | from .user import User 8 | 9 | 10 | class AuthError(Exception): 11 | pass 12 | 13 | 14 | class ThrottleError(Exception): 15 | pass 16 | 17 | 18 | class Throne(object): 19 | """ 20 | Provides access to Throne.ai API 21 | 22 | import peyton 23 | throne = peyton.Throne(username='goat', token='your-token-here') 24 | 25 | """ 26 | 27 | BASE_URL = 'https://www.throne.ai/api/' 28 | 29 | def __enter__(self): 30 | return self 31 | 32 | def __exit__(self, *_args): 33 | pass 34 | 35 | def __init__(self, username, token): 36 | """ 37 | Initializes and tests a connection 38 | """ 39 | 40 | self.username = username 41 | self.token = token 42 | self.headers = {'Authorization': self.token, 'X-USERNAME': self.username} 43 | self._test_auth() 44 | 45 | self.competition = Competition(api=self) 46 | self.user = User(api=self) 47 | 48 | @staticmethod 49 | def _auth_report(result): 50 | """ 51 | Raises authorization error if credentials are invalid 52 | """ 53 | 54 | if result.status_code == 403: 55 | raise AuthError('Invalid credentials') 56 | 57 | if result.status_code == 429: 58 | raise ThrottleError(json.loads(result.content.decode("utf-8"))['detail']) 59 | 60 | def _test_auth(self): 61 | """ 62 | Tests credentials upon initialization 63 | """ 64 | result = requests.get('%s%s' % (self.BASE_URL, 'auth/'), headers=self.headers) 65 | self._auth_report(result) 66 | 67 | def omaha(self): 68 | """ 69 | Make an audible 70 | """ 71 | x = random.randint(0, 5) 72 | 73 | print(random_play(x)) -------------------------------------------------------------------------------- /peyton/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import matplotlib.pyplot as plt 4 | import pandas as pd 5 | import requests 6 | 7 | class User(object): 8 | 9 | def __init__(self, api): 10 | """ 11 | Contains methods for retrieving user based data 12 | """ 13 | self.api = api # Throne API instance with auth information 14 | 15 | def get_xp(self): 16 | """ 17 | Retrieves User XP 18 | 19 | Returns 20 | -------- 21 | int - the XP of the user 22 | """ 23 | result = requests.get('%s%s' % (self.api.BASE_URL, 'user/xp/'), headers=self.api.headers) 24 | self.api._auth_report(result) 25 | 26 | return json.loads(result.content.decode("utf-8"))['result']['xp'] 27 | 28 | def get_performance(self): 29 | """ 30 | Retrieves User XP 31 | 32 | Returns 33 | -------- 34 | dict - a dictionary of user performance data 35 | """ 36 | result = requests.get('%s%s' % (self.api.BASE_URL, 'user/performance/'), headers=self.api.headers) 37 | self.api._auth_report(result) 38 | content = json.loads(result.content.decode("utf-8"))['result'] 39 | 40 | self.competitions_entered = [name for name in content.keys() if name != 'total'] 41 | self.score_dfs = {} 42 | 43 | # make dataframes for the historical scores for each competition 44 | for comp_key, comp_values in content.items(): 45 | if comp_key == 'total': 46 | continue 47 | 48 | self.score_dfs[comp_key] = pd.DataFrame(content[comp_key]['score_series'][1:]) 49 | self.score_dfs[comp_key].columns = ['Throne Score'] 50 | self.score_dfs[comp_key].index = [datetime.datetime.strptime(date, '%Y-%m-%dT%H:%M:%S') for date in content[comp_key]['score_dates']] 51 | 52 | self.edge_df = pd.DataFrame.from_dict({comp_key : comp_values['edge'] for comp_key, comp_values in content.items() if comp_key != 'total'}, orient='index') 53 | self.edge_df.columns = ['Edge'] 54 | 55 | return content 56 | 57 | def plot_edge(self): 58 | """ 59 | Plots the user edge for each competition 60 | 61 | Returns 62 | -------- 63 | A plot of the user score for the competition 64 | """ 65 | 66 | if not hasattr(self, 'edge_df'): 67 | self.get_performance() 68 | 69 | try: 70 | self.edge_df.plot.bar(rot=0); plt.axhline(0, color='k'); plt.title("Edge for Competitions"); plt.show() 71 | except AttributeError: 72 | raise AttributeError('You have no records in any competition') 73 | 74 | def plot_score(self, competition): 75 | """ 76 | Plots the score for a given competition 77 | 78 | Parameters 79 | -------- 80 | competition - str 81 | The name of the competition, e.g. "Spanish La Liga" 82 | 83 | Returns 84 | -------- 85 | A plot of the user score for the competition 86 | """ 87 | 88 | if not hasattr(self, 'score_dfs'): 89 | self.get_performance() 90 | 91 | try: 92 | self.score_dfs[competition].plot.area(); plt.show() 93 | except AttributeError: 94 | raise AttributeError('You have no records in any competition') 95 | except KeyError: 96 | raise KeyError('You have no record for a competition called %s. You are only entered in competitions: %s ' % (competition, self.competitions_entered)) -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [metadata] 5 | license_file = LICENSE -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import re 2 | from codecs import open 3 | from os import path 4 | from setuptools import find_packages, setup 5 | 6 | 7 | PACKAGE_NAME = 'peyton' 8 | VERSION = '0.2.0' 9 | 10 | 11 | setup(name=PACKAGE_NAME, 12 | author='Ross Taylor', 13 | author_email='rj-taylor@live.co.uk', 14 | classifiers=[ 15 | 'Development Status :: 5 - Production/Stable', 16 | 'Environment :: Console', 17 | 'Intended Audience :: Developers', 18 | 'License :: OSI Approved :: BSD License', 19 | 'Natural Language :: English', 20 | 'Operating System :: OS Independent', 21 | 'Programming Language :: Python', 22 | 'Programming Language :: Python :: 3.5', 23 | 'Programming Language :: Python :: 3.6', 24 | 'Programming Language :: Python :: Implementation :: CPython', 25 | 'Topic :: Utilities'], 26 | description=('Peyton, an API wrapper for the Throne.AI Platform'), 27 | keywords='throne api wrapper', 28 | packages=find_packages(exclude=['tests', 'tests.*']), 29 | license='MIT', 30 | url='https://throne.ai/', 31 | version=VERSION) --------------------------------------------------------------------------------