├── CFBScrapy.egg-info ├── dependency_links.txt ├── top_level.txt ├── requires.txt ├── SOURCES.txt └── PKG-INFO ├── CFBScrapy ├── __init__.py ├── __pycache__ │ ├── index.cpython-37.pyc │ ├── __init__.cpython-37.pyc │ └── cfbtools.cpython-37.pyc └── cfbtools.py ├── requirements.txt ├── dist ├── CFBScrapy-0.1.6.tar.gz ├── CFBScrapy-0.1.7.tar.gz ├── CFBScrapy-0.1.6-py3-none-any.whl └── CFBScrapy-0.1.7-py3-none-any.whl ├── README.md └── setup.py /CFBScrapy.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CFBScrapy.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | CFBScrapy 2 | -------------------------------------------------------------------------------- /CFBScrapy/__init__.py: -------------------------------------------------------------------------------- 1 | from CFBScrapy.cfbtools import * -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==0.23.4 2 | requests==2.21.0 -------------------------------------------------------------------------------- /CFBScrapy.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | pandas==0.23.4 2 | requests==2.21.0 3 | -------------------------------------------------------------------------------- /dist/CFBScrapy-0.1.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/dist/CFBScrapy-0.1.6.tar.gz -------------------------------------------------------------------------------- /dist/CFBScrapy-0.1.7.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/dist/CFBScrapy-0.1.7.tar.gz -------------------------------------------------------------------------------- /dist/CFBScrapy-0.1.6-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/dist/CFBScrapy-0.1.6-py3-none-any.whl -------------------------------------------------------------------------------- /dist/CFBScrapy-0.1.7-py3-none-any.whl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/dist/CFBScrapy-0.1.7-py3-none-any.whl -------------------------------------------------------------------------------- /CFBScrapy/__pycache__/index.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/CFBScrapy/__pycache__/index.cpython-37.pyc -------------------------------------------------------------------------------- /CFBScrapy/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/CFBScrapy/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /CFBScrapy/__pycache__/cfbtools.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rlindholm/CFBScrapy/HEAD/CFBScrapy/__pycache__/cfbtools.cpython-37.pyc -------------------------------------------------------------------------------- /CFBScrapy.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | README.md 2 | setup.py 3 | CFBScrapy/__init__.py 4 | CFBScrapy/cfbtools.py 5 | CFBScrapy.egg-info/PKG-INFO 6 | CFBScrapy.egg-info/SOURCES.txt 7 | CFBScrapy.egg-info/dependency_links.txt 8 | CFBScrapy.egg-info/requires.txt 9 | CFBScrapy.egg-info/top_level.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CFBScrapy 2 | Python wrapper for the collegefootballapi located here: https://api.collegefootballdata.com/api/docs/?url=/api-docs.json#/ 3 | 4 | ## Installation 5 | 6 | Can be installed using PyPi using pip install CFBScrapy 7 | 8 | ## Usage 9 | 10 | ```python 11 | import CFBScrapy as cfb 12 | t = cfb.get_game_info(year=2018) 13 | ``` 14 | 15 | 16 | ## Contributing 17 | 18 | Feel free to open any issues or pull requests. Additionally, you can reach out to me at ryan.lindholm@outlook.com with any questions or concerns. 19 | 20 | ## Roadmap 21 | 22 | 1. Scraping 247 player rankings to get the individual player rankings 23 | 2. EPA and Win probability a la nflscrapR 24 | 3. Normalization of the tables returned 25 | 26 | Author: Ryan Lindholm 27 | 28 | Contributors: Meyappan Subbaiah 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | 3 | from setuptools import setup 4 | from setuptools import find_packages, setup 5 | 6 | HERE = pathlib.Path(__file__).parent 7 | 8 | README = (HERE/ "README.md").read_text() 9 | 10 | with open('requirements.txt') as f: 11 | REQUIRED = f.read().splitlines() 12 | 13 | 14 | setup( 15 | name="CFBScrapy", 16 | version="0.1.07", 17 | description="Python wrapper for the collegefootballapi located here: https://api.collegefootballdata.com/api/docs/?url=/api-docs.json#/", 18 | long_description=README, 19 | long_description_content_type="text/markdown", 20 | url="https://github.com/rlindholm/CFBScrapy", 21 | author="Ryan Lindholm", 22 | author_email="ryan.lindholm@outlook.com", 23 | packages=find_packages(), 24 | install_requires=[REQUIRED], 25 | classifiers=[ 26 | 'Programming Language :: Python :: 3.6', 27 | "Operating System :: OS Independent" 28 | ], 29 | #install_requires=["requests", "pandas", "pandas.io"], 30 | 31 | ) 32 | -------------------------------------------------------------------------------- /CFBScrapy.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: CFBScrapy 3 | Version: 0.1.7 4 | Summary: Python wrapper for the collegefootballapi located here: https://api.collegefootballdata.com/api/docs/?url=/api-docs.json#/ 5 | Home-page: https://github.com/rlindholm/CFBScrapy 6 | Author: Ryan Lindholm 7 | Author-email: ryan.lindholm@outlook.com 8 | License: UNKNOWN 9 | Description: # CFBScrapy 10 | Python wrapper for the collegefootballapi located here: https://api.collegefootballdata.com/api/docs/?url=/api-docs.json#/ 11 | 12 | ## Installation 13 | 14 | Can be installed using PyPi using pip install CFBScrapy 15 | 16 | ## Usage 17 | 18 | ```python 19 | import CFBScrapy as cfb 20 | t = cfb.get_game_info(year=2018) 21 | ``` 22 | 23 | 24 | ## Contributing 25 | 26 | Feel free to open any issues or pull requests. Additionally, you can reach out to me at ryan.lindholm@outlook.com with any questions or concerns. 27 | 28 | ## Roadmap 29 | 30 | 1. Scraping 247 player rankings to get the individual player rankings 31 | 2. EPA and Win probability a la nflscrapR 32 | 3. Normalization of the tables returned 33 | 34 | Author: Ryan Lindholm 35 | 36 | Contributors: Meyappan Subbaiah 37 | 38 | Platform: UNKNOWN 39 | Classifier: Programming Language :: Python :: 3.6 40 | Classifier: Operating System :: OS Independent 41 | Description-Content-Type: text/markdown 42 | -------------------------------------------------------------------------------- /CFBScrapy/cfbtools.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pandas as pd 3 | import json 4 | from pandas.io.json import json_normalize 5 | 6 | 7 | def get_game_info(year, seasonType='regular', week=None, team=None, home=None, 8 | away=None, conference=None): 9 | ''' 10 | Takes in year, seasonType, week, team, hometeam, awayteam or conference 11 | and returns a DataFrame containing results and demographic information 12 | for all of the games in the queried time period, team or season 13 | 14 | year = year. 15 | seasonType = regular or postseason. default is regular 16 | week(optional) = week. 17 | team(optional) = see get_team_info() for valid team names, queries 18 | for a team regardless of their role in the game (home or away). 19 | home(optional) = queries by home team. Ex. home=Florida State will return a 20 | DataFrame containing info from all of Florida State's games. 21 | away(optional) = queries by away team. 22 | conference(optional) = queries by conference. Ex.conference=SEC will return a 23 | DataFrame containing info from all SEC games. 24 | ''' 25 | base_url = 'https://api.collegefootballdata.com/games?' 26 | payload = {} 27 | payload['year'] = year 28 | payload['seasonType'] = seasonType 29 | payload['week'] = week 30 | payload['team'] = team 31 | payload['home'] = home 32 | payload['away'] = away 33 | payload['conference'] = conference 34 | 35 | r = requests.get(base_url, params=payload) 36 | if r.status_code == 200: 37 | return pd.DataFrame(r.json()) 38 | else: 39 | raise Exception('Request failed with status code: '+str(r.status_code)) 40 | 41 | 42 | 43 | def get_game_player_stats(year, week=None, seasonType='regular', team=None, 44 | conference=None, category=None, gameId=None): 45 | ''' 46 | Takes in year, seasonType, week, team, home, away, conference, or gameID 47 | and returns a DataFrame with all of the individual player stats for the given 48 | parameters 49 | 50 | Must have either a gameID, week, team or conference. 51 | JSON required special parsing in order to massage it into one DataFrame. 52 | 53 | year = year. 54 | week (optional) = week. 55 | seasonType = regular or postseason. 56 | conference(optional) = see get_conference_list() to see list of valid conferences, 57 | query by conference. 58 | category(optional) = valid categories: defensive, fumbles, punting, kicking, 59 | puntReturns, interceptions, receiving, rushing, passing, 60 | kickReturns 61 | 62 | TODO break totalPenaltiesYards, fourthDownEff, thirdDownEff and completionAttempts 63 | into seperate stats instead of compound stats 64 | ''' 65 | base_url = 'https://api.collegefootballdata.com/games/players?' 66 | payload = {} 67 | payload['year'] = year 68 | payload['seasonType'] = seasonType 69 | 70 | #if one of these conditions is not true it will return a bad request 71 | if week is not None or team is not None or conference is not None or gameId is not None: 72 | payload['week'] = week 73 | payload['team'] = team 74 | payload['conference'] = conference 75 | payload['category'] = category 76 | payload['gameId'] = gameId 77 | else: 78 | raise ValueError('Must have 1 of team, week, conference or a valid gameID') 79 | 80 | r = requests.get(base_url, params=payload) 81 | 82 | #nested json, have to normalize at multiple levels 83 | #creating dataframes containing non nested info then merging back 84 | #clean this up if you can think of a better way to bring in the meta from the parent 85 | if r.status_code == 200: 86 | try: 87 | games = r.json() 88 | games_df = json_normalize(games)[['id']] 89 | teams_df = json_normalize(games, record_path='teams', meta='id') 90 | categories_df = json_normalize(json.loads(teams_df.to_json(orient='records')), 91 | record_path='categories', meta=['id','school']) 92 | types_df = json_normalize(json.loads(categories_df.to_json(orient='records')), 93 | record_path = 'types', meta = ['id','name','school'], 94 | record_prefix = 'play_type_') 95 | athletes_df = json_normalize(json.loads(types_df.to_json(orient='records')), 96 | record_path = 'play_type_athletes', 97 | meta = ['school','name','id','play_type_name'], 98 | record_prefix = 'athlete_') 99 | 100 | 101 | return games_df.merge(teams_df[['conference','homeAway','points','school','id']])\ 102 | .merge(categories_df[['name','id','school']])\ 103 | .merge(types_df[['play_type_name','id','name','school']])\ 104 | .merge(athletes_df) 105 | except KeyError: 106 | raise KeyError('Invalid parameters, no results returned') 107 | 108 | else: 109 | raise Exception('Request failed with status code: '+str(r.status_code)) 110 | 111 | 112 | def get_game_team_stats(year, week=None, seasonType='regular', team=None, conference=None, 113 | gameId=None): 114 | ''' 115 | Takes in year, week, seasonType, team, conference, or gameID and returns 116 | a DataFrame showing all of the team stats for each game in the queried parameters 117 | 118 | year=year 119 | week (optional)=week. 120 | seasonType (optional) = regular or postseason default to regular 121 | team (optional) = returns a dataframe containing team stats from each of the 122 | games played by a team in a given season. 123 | conference (optional) = returns a dataframe containing all of the team stats 124 | from each of the games played by a team in that 125 | conference in a given season. 126 | 127 | TODO break totalPenaltiesYards, fourthDownEff, thirdDownEff and completionAttempts 128 | into seperate stats 129 | 130 | ''' 131 | base_url = 'https://api.collegefootballdata.com/games/teams?' 132 | payload = {} 133 | payload['year'] = year 134 | payload['seasonType'] = seasonType 135 | 136 | #needs one of these to not return a bad request 137 | if week is not None or team is not None or conference is not None or gameId is not None: 138 | payload['week'] = week 139 | payload['team'] = team 140 | payload['conference'] = conference 141 | payload['gameId'] = gameId 142 | else: 143 | raise ValueError('Must have 1 of team, week, conference or a valid gameID') 144 | 145 | #deeply nested json again 146 | r = requests.get(base_url, params=payload) 147 | if r.status_code == 200: 148 | try: 149 | games = r.json() 150 | games_df = json_normalize(r.json())[['id']] 151 | teams_df = json_normalize(games, record_path='teams', meta='id') 152 | stats_df = json_normalize(json.loads(teams_df.to_json(orient='records')), 153 | record_path='stats', meta=['school','id']) 154 | 155 | return games_df.merge(teams_df[['conference','homeAway','points','school','id']])\ 156 | .merge(stats_df) 157 | 158 | except KeyError: 159 | raise KeyError('Invalid parameters, no results returned') 160 | 161 | else: 162 | raise Exception('Request failed with status code: '+str(r.status_code)) 163 | 164 | 165 | def get_drive_info(year, seasonType='regular', week=None, team=None, offense=None, defense=None, conference=None, offenseConference=None, defenseConference=None): 166 | ''' 167 | Takes in year, seasonType, week, team, conference or gameId and returns 168 | a DataFrame containing the drive info from each of the games in the queried 169 | parameters 170 | 171 | year=year 172 | seasonType (optional) = regular or postseason. Default: regular season 173 | week (optional) =week 174 | team (optional) =Gets all of the drives by the queried team in a given season 175 | offense (optional) =Gets all of the drives by the queried offense in a given season 176 | defense (optional) =Gets all of the drives the queried defense participated in in a given season 177 | 178 | TODO Break elapsed, start time, end time out into time datatypes 179 | ''' 180 | 181 | base_url = 'https://api.collegefootballdata.com/drives?' 182 | payload = {} 183 | payload['year'] = year 184 | payload['seasonType'] = seasonType 185 | payload['week'] = week 186 | payload['team'] = team 187 | payload['conference'] = conference 188 | payload['offense'] = offense 189 | payload['defense'] = defense 190 | payload['offenseConference'] = offenseConference 191 | payload['defenseConference'] = defenseConference 192 | 193 | r = requests.get(base_url, params=payload) 194 | if r.status_code == 200: 195 | return pd.DataFrame(r.json()) 196 | else: 197 | raise Exception('Request failed with status code: '+str(r.status_code)) 198 | 199 | 200 | def get_play_by_play_data(year, seasonType='regular', week=None, team=None, offense=None, 201 | defense=None, conference=None, offenseConference=None, 202 | defenseConference=None, playType=None): 203 | ''' 204 | Takes in year, seasonType, week, team, offense, defense, conference, 205 | offenseConference, defenseConference or playType and returns 206 | a DataFrame containing all of the plays for the games by the queried 207 | parameters. 208 | 209 | year = year 210 | seasonType (optional) = regular or postseason. Default='regular' 211 | week=week 212 | team=returns all the plays where the queried team was involved 213 | offense=returns all of the plays where the queried offense was involved 214 | defense=returns all of the plays where the queried defense was involved 215 | conference=returns all of the plays where the queried conference was involved 216 | offenseConference=returns all of the plays where the offenseConference is the 217 | queried conference 218 | defenseConference= returns all of the plays where the defenseConference is the 219 | queried conference 220 | playType=returns all of the plays of the queried type in a given season. 221 | 222 | TODO break clock into time format 223 | ''' 224 | base_url = 'https://api.collegefootballdata.com/plays?' 225 | payload = {} 226 | payload['year'] = year 227 | payload['seasonType'] = seasonType 228 | payload['week'] = week 229 | payload['team'] = team 230 | payload['conference'] = conference 231 | payload['offense'] = offense 232 | payload['defense'] = defense 233 | payload['offenseConference'] = offenseConference 234 | payload['defenseConference'] = defenseConference 235 | payload['playType'] = playType 236 | 237 | r = requests.get(base_url, params=payload) 238 | if r.status_code == 200: 239 | return pd.DataFrame(r.json()) 240 | else: 241 | raise Exception('Request failed with status code: '+str(r.status_code)) 242 | 243 | 244 | def get_play_types(): 245 | ''' 246 | Returns a DataFrame containing all possible play types 247 | in the play by play data 248 | ''' 249 | r = requests.get('https://api.collegefootballdata.com/play/types') 250 | 251 | if r.status_code == 200: 252 | return pd.DataFrame(r.json()) 253 | else: 254 | raise Exception('Request failed with status code: '+str(r.status_code)) 255 | 256 | 257 | def get_team_info(conference=None): 258 | ''' 259 | Returns a DataFrame containing color, logo, and mascot info 260 | about each team in the queried params 261 | 262 | conference (optional) = queries by conference 263 | ''' 264 | base_url = 'https://api.collegefootballdata.com/teams' 265 | payload = {} 266 | 267 | if conference is not None: 268 | payload['conference'] = conference 269 | 270 | r = requests.get(base_url, params=payload) 271 | if r.status_code == 200: 272 | return pd.DataFrame(r.json()) 273 | else: 274 | raise Exception('Request failed with status code: '+str(r.status_code)) 275 | 276 | 277 | def get_team_roster(team): 278 | ''' 279 | Returns a DataFrame containing all of the players on a given roster 280 | 281 | team=team 282 | ''' 283 | base_url = 'https://api.collegefootballdata.com/roster?' 284 | payload = {} 285 | 286 | payload['team'] = team 287 | 288 | r = requests.get(base_url, params=payload) 289 | if r.status_code == 200: 290 | return pd.DataFrame(r.json()) 291 | else: 292 | raise Exception('Request failed with status code: '+str(r.status_code)) 293 | 294 | 295 | def get_team_talent(year=None): 296 | ''' 297 | Shows the team talent rankings of every team 298 | 299 | year (optional) = year 300 | ''' 301 | base_url = 'https://api.collegefootballdata.com/talent' 302 | payload = {} 303 | 304 | payload['year'] = year 305 | 306 | r = requests.get(base_url, params=payload) 307 | if r.status_code == 200: 308 | return pd.DataFrame(r.json()) 309 | else: 310 | raise Exception('Request failed with status code: '+str(r.status_code)) 311 | 312 | 313 | def get_matchup_history(team1, team2, minYear=None, maxYear=None): 314 | ''' 315 | Takes in team1, team2, minYear, maxYear and returns a DataFrame 316 | containing the record and information about each of the times 317 | the two teams have met. 318 | 319 | team1 = team1 320 | team2 = team2 321 | minYear (optional) = lowest year to consider 322 | maxYear (optional) = highest year to consider 323 | 324 | ''' 325 | base_url = 'https://api.collegefootballdata.com/teams/matchup?' 326 | payload = {} 327 | 328 | payload['team1'] = team1 329 | payload['team2'] = team2 330 | payload['minYear'] = minYear 331 | payload['maxYear'] = maxYear 332 | 333 | r = requests.get(base_url, params=payload) 334 | if r.status_code == 200: 335 | return json_normalize(r.json(), record_path='games', 336 | meta=['team1','team1Wins','team2','team2Wins','ties']) 337 | else: 338 | raise Exception('Request failed with status code: '+str(r.status_code)) 339 | 340 | 341 | def get_conference_list(): 342 | ''' 343 | Returns a DataFrame containing a list of conferences and their full 344 | names 345 | ''' 346 | r = requests.get('https://api.collegefootballdata.com/conferences') 347 | if r.status_code == 200: 348 | return pd.DataFrame(r.json()) 349 | else: 350 | raise Exception('Request failed with status code: '+str(r.status_code)) 351 | 352 | 353 | def get_venue_info(): 354 | ''' 355 | Returns a DataFrame containing a list of the different venues, 356 | and information about them such as location, elevation and type of turf 357 | 358 | TODO break location into lat and long columns 359 | ''' 360 | r = requests.get('https://api.collegefootballdata.com/venues') 361 | if r.status_code == 200: 362 | return pd.DataFrame(r.json()) 363 | else: 364 | raise Exception('Request failed with status code: '+str(r.status_code)) 365 | 366 | 367 | def get_coach_info(firstName=None, lastName=None, team=None, year=None, minYear=None, 368 | maxYear=None): 369 | ''' 370 | Takes in first name, last name, team, year, and a range of years and 371 | returns a DataFrame containing a list of the different coaches, how many wins 372 | they got each season, where they were and ranks before and after the season 373 | 374 | firstName (optional) = first name of the coach 375 | lastName (optional) = last name of the coach 376 | team (optional) = team 377 | year (optional) = year 378 | minYear (optional) = minimum year considered 379 | maxYear (optional) = max year considered 380 | ''' 381 | base_url = 'https://api.collegefootballdata.com/coaches' 382 | payload = {} 383 | payload['firstName'] = firstName 384 | payload['lastName'] = lastName 385 | payload['team'] = team 386 | payload['year'] = year 387 | payload['minYear'] = minYear 388 | payload['maxYear'] = maxYear 389 | 390 | r = requests.get(base_url, params=payload) 391 | if r.status_code == 200: 392 | return json_normalize(r.json(),record_path = 'seasons', meta=['first_name','last_name']) 393 | else: 394 | raise Exception('Request failed with status code: '+str(r.status_code)) 395 | 396 | 397 | def get_historical_rankings(year, week=None, seasonType=None): 398 | ''' 399 | Take in year, week and season type and print a DataFrame containing 400 | each teams ranking within each poll in a given season and week 401 | 402 | Year: int, year 403 | Week: int, week 404 | seasonType: string, values: regular or postseason 405 | ''' 406 | 407 | base_url = 'https://api.collegefootballdata.com/rankings?' 408 | payload = {} 409 | 410 | payload['year'] = year 411 | payload['week'] = week 412 | payload['seasonType'] = seasonType 413 | 414 | r = requests.get(base_url, params=payload) 415 | if r.status_code == 200: 416 | polls_df = json_normalize(r.json(), record_path = 'polls', 417 | meta=['season','seasonType','week']) 418 | return json_normalize(json.loads(polls_df.to_json(orient='records')), 419 | record_path = 'ranks', 420 | meta=['season','seasonType','week','poll']) 421 | else: 422 | raise Exception('Request failed with status code: '+str(r.status_code)) 423 | 424 | 425 | def get_betting_lines(year, week=None, seasonType=None, team=None, home=None, away=None, conference=None): 426 | ''' 427 | Takes in year, week, season type, team, home, away, and conference and returns 428 | a DataFrame containing betting lines from different sources for each game. 429 | 430 | Year: int, year 431 | Week: int, week 432 | seasonType: string, values: regular or postseason 433 | Team: string, team 434 | Home: string, home team filter 435 | Away: string, away team filter 436 | Conference: string, conference filter 437 | ''' 438 | 439 | base_url = 'https://api.collegefootballdata.com/lines?' 440 | payload = {} 441 | 442 | payload['year'] = year 443 | payload['week'] = week 444 | payload['seasonType'] = seasonType 445 | payload['team'] = team 446 | payload['home'] = home 447 | payload['away'] = away 448 | payload['conference'] = conference 449 | 450 | r = requests.get(base_url, params=payload) 451 | if r.status_code == 200: 452 | try: 453 | bets_df = json_normalize(r.json())[['id', 'homeTeam', 'awayTeam', 'awayScore']] 454 | lines_df = json_normalize(r.json(), errors='ignore', record_path='lines', meta=['id']) 455 | return bets_df.merge(lines_df) 456 | 457 | except KeyError: 458 | raise KeyError('Invalid parameters, no results returned') 459 | 460 | else: 461 | raise Exception('Request failed with status code: ' + str(r.status_code)) --------------------------------------------------------------------------------