From 6162c035f3e7e26db6871313b90db0b9d19977fc Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 5 Apr 2021 10:46:44 -0700 Subject: [PATCH 1/2] Initial Baseball Implementation --- README.md | 8 +- espn_api/baseball/__init__.py | 10 ++ espn_api/baseball/activity.py | 30 ++++++ espn_api/baseball/constant.py | 98 ++++++++++++++++++ espn_api/baseball/league.py | 120 ++++++++++++++++++++++ espn_api/baseball/matchup.py | 47 +++++++++ espn_api/baseball/player.py | 24 +++++ espn_api/baseball/team.py | 63 ++++++++++++ espn_api/baseball/utils.py | 21 ++++ tests/baseball/__init__.py | 0 tests/baseball/integration/__init__.py | 0 tests/baseball/integration/test_league.py | 23 +++++ 12 files changed, 442 insertions(+), 2 deletions(-) create mode 100644 espn_api/baseball/__init__.py create mode 100644 espn_api/baseball/activity.py create mode 100644 espn_api/baseball/constant.py create mode 100644 espn_api/baseball/league.py create mode 100644 espn_api/baseball/matchup.py create mode 100644 espn_api/baseball/player.py create mode 100644 espn_api/baseball/team.py create mode 100644 espn_api/baseball/utils.py create mode 100644 tests/baseball/__init__.py create mode 100644 tests/baseball/integration/__init__.py create mode 100644 tests/baseball/integration/test_league.py diff --git a/README.md b/README.md index 20ccbf08..29877b2d 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ ![](https://github.com/cwendt94/espn-api/workflows/Espn%20API%20Integration%20Test/badge.svg) [![codecov](https://codecov.io/gh/cwendt94/espn-api/branch/master/graphs/badge.svg)](https://codecov.io/gh/cwendt94/espn-api) [![Join the chat at https://gitter.im/ff-espn-api/community](https://badges.gitter.im/ff-espn-api/community.svg)](https://gitter.im/ff-espn-api/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![PyPI version](https://badge.fury.io/py/espn-api.svg)](https://badge.fury.io/py/espn-api) ## ESPN API -## [NOTICE] Currently username and password are not working, ESPN recently changed their authentication. You can still access your private league using SWID and ESPN_S2. -This package uses ESPN's Fantasy API to extract data from any public or private league for **Fantasy Football and Basketball**. +## [NOTICE] Username and password will be removed soon. You can access your private league using SWID and ESPN_S2. +This package uses ESPN's Fantasy API to extract data from any public or private league for **Fantasy Football and Basketball (Hockey and Baseball in development)**. Please feel free to make suggestions, bug reports, and pull request for features or fixes! This package was inspired and based off of [rbarton65/espnff](https://github.com/rbarton65/espnff). @@ -27,6 +27,10 @@ pip install espn_api from espn_api.football import League # Basketball API from espn_api.basketball import League +# Hockey API +from espn_api.hockey import League +# Baseball API +from espn_api.baseball import League # Init league = League(league_id=222, year=2019) ``` diff --git a/espn_api/baseball/__init__.py b/espn_api/baseball/__init__.py new file mode 100644 index 00000000..898b3e84 --- /dev/null +++ b/espn_api/baseball/__init__.py @@ -0,0 +1,10 @@ +__all__ = ['League', + 'Team', + 'Player', + 'Matchup', + ] + +from .league import League +from .team import Team +from .player import Player +from .matchup import Matchup \ No newline at end of file diff --git a/espn_api/baseball/activity.py b/espn_api/baseball/activity.py new file mode 100644 index 00000000..439a6175 --- /dev/null +++ b/espn_api/baseball/activity.py @@ -0,0 +1,30 @@ +from .constant import ACTIVITY_MAP + +class Activity(object): + def __init__(self, data, player_map, get_team_data): + self.actions = [] # List of tuples (Team, action, player) + self.date = data['date'] + for msg in data['messages']: + team = '' + action = 'UNKNOWN' + player = '' + msg_id = msg['messageTypeId'] + if msg_id == 244: + team = get_team_data(msg['from']) + elif msg_id == 239: + team = get_team_data(msg['for']) + else: + team = get_team_data(msg['to']) + if msg_id in ACTIVITY_MAP: + action = ACTIVITY_MAP[msg_id] + if msg['targetId'] in player_map: + player = player_map[msg['targetId']] + self.actions.append((team, action, player)) + + def __repr__(self): + return 'Activity(' + ' '.join("(%s,%s,%s)" % tup for tup in self.actions) + ')' + + + + + diff --git a/espn_api/baseball/constant.py b/espn_api/baseball/constant.py new file mode 100644 index 00000000..926d7952 --- /dev/null +++ b/espn_api/baseball/constant.py @@ -0,0 +1,98 @@ +POSITION_MAP = { + 0: 'C', + 1: '1B', + 2: '2B', + 3: '3B', + 4: 'SS', + 5: 'OF', + 6: '2B/SS', + 7: '1B/3B', + 8: 'DH', + 9: '9', # For unknown use stat number + 10: '10', + 11: 'DH', + 12: 'UTIL', + 13: 'P', + 14: 'SP', + 15: 'RP', + 16: 'BE', + 17: 'IL', + 18: '18', + 19: '19' + # reverse TODO +} + +PRO_TEAM_MAP = { + 0: 'FA', + 1: 'Bal', + 2: 'Bos', + 3: 'LAA', + 4: 'ChW', + 5: 'Cle', + 6: 'Det', + 7: 'KC', + 8: 'Mil', + 9: 'Min', + 10: 'NYY', + 11: 'Oak', + 12: 'Sea', + 13: 'Tex', + 14: 'Tor', + 15: 'Atl', + 16: 'ChC', + 17: 'Cin', + 18: 'Hou', + 19: 'LAD', + 20: 'Wsh', + 21: 'NYM', + 22: 'Phi', + 23: 'Pit', + 24: 'StL', + 25: 'SD', + 26: 'SF', + 27: 'Col', + 28: 'Mia', + 29: 'Ari', + 30: 'TB', +} + +STATS_MAP = { + 0: 'AB', + 1: 'H', + 2: 'AVG', + 5: 'HR', + 10: 'BB', + 16: 'PA', + 17: 'OBP', + 20: 'R', + 21: 'RBI', + 23: 'SB', + 34: 'OUTS', + 35: 'BATTERS', + 36: 'PITCHES', + 37: 'P_H', + 39: 'P_BB', + 41: 'WHIP', + 42: 'HBP', + 44: 'P_R', + 45: 'ER', + 46: 'P_HR', + 47: 'ERA', + 48: 'K', + 53: 'W', + 54: 'L', + 57: 'SV', + 99: 'STARTER', +} + +ACTIVITY_MAP = { + 178: 'FA ADDED', + 180: 'WAIVER ADDED', + 179: 'DROPPED', + 181: 'DROPPED', + 239: 'DROPPED', + 244: 'TRADED', + 'FA': 178, + 'WAIVER': 180, + 'TRADED': 244 +} diff --git a/espn_api/baseball/league.py b/espn_api/baseball/league.py new file mode 100644 index 00000000..df7eff63 --- /dev/null +++ b/espn_api/baseball/league.py @@ -0,0 +1,120 @@ +import datetime +import time +import json +import math +from typing import List, Tuple +import pdb + +from ..base_league import BaseLeague +from .team import Team +from .player import Player +from .matchup import Matchup +from .constant import PRO_TEAM_MAP +from.activity import Activity +from .constant import POSITION_MAP, ACTIVITY_MAP + +class League(BaseLeague): + '''Creates a League instance for Public/Private ESPN league''' + def __init__(self, league_id: int, year: int, espn_s2=None, swid=None, username=None, password=None, debug=False): + super().__init__(league_id=league_id, year=year, sport='mlb', espn_s2=espn_s2, swid=swid, username=username, password=password, debug=debug) + + data = self._fetch_league() + self._fetch_teams(data) + + def _fetch_league(self): + data = super()._fetch_league() + self._fetch_players() + return(data) + + def _fetch_teams(self, data): + '''Fetch teams in league''' + super()._fetch_teams(data, TeamClass=Team) + + # replace opponentIds in schedule with team instances + for team in self.teams: + team.division_name = self.settings.division_map.get(team.division_id, '') + for week, matchup in enumerate(team.schedule): + for opponent in self.teams: + if matchup.away_team == opponent.team_id: + matchup.away_team = opponent + if matchup.home_team == opponent.team_id: + matchup.home_team = opponent + + def standings(self) -> List[Team]: + standings = sorted(self.teams, key=lambda x: x.final_standing if x.final_standing != 0 else x.standing, reverse=False) + return standings + + def scoreboard(self, matchupPeriod: int = None) -> List[Matchup]: + '''Returns list of matchups for a given matchup period''' + if not matchupPeriod: + matchupPeriod=self.currentMatchupPeriod + + params = { + 'view': 'mMatchup', + } + data = self.espn_request.league_get(params=params) + schedule = data['schedule'] + matchups = [Matchup(matchup) for matchup in schedule if matchup['matchupPeriodId'] == matchupPeriod] + + for team in self.teams: + for matchup in matchups: + if matchup.home_team == team.team_id: + matchup.home_team = team + elif matchup.away_team == team.team_id: + matchup.away_team = team + + return matchups + + def get_team_data(self, team_id: int) -> Team: + for team in self.teams: + if team_id == team.team_id: + return team + return None + + def recent_activity(self, size: int = 25, msg_type: str = None) -> List[Activity]: + '''Returns a list of recent league activities (Add, Drop, Trade)''' + if self.year < 2019: + raise Exception('Cant use recent activity before 2019') + + msg_types = [178,180,179,239,181,244] + if msg_type in ACTIVITY_MAP: + msg_types = [ACTIVITY_MAP[msg_type]] + params = { + 'view': 'kona_league_communication' + } + + filters = {"topics":{"filterType":{"value":["ACTIVITY_TRANSACTIONS"]},"limit":size,"limitPerMessageSet":{"value":25},"offset":0,"sortMessageDate":{"sortPriority":1,"sortAsc":False},"sortFor":{"sortPriority":2,"sortAsc":False},"filterIncludeMessageTypeIds":{"value":msg_types}}} + headers = {'x-fantasy-filter': json.dumps(filters)} + data = self.espn_request.league_get(extend='/communication/', params=params, headers=headers) + data = data['topics'] + activity = [Activity(topic, self.player_map, self.get_team_data) for topic in data] + + return activity + + def free_agents(self, week: int=None, size: int=50, position: str=None, position_id: int=None) -> List[Player]: + '''Returns a List of Free Agents for a Given Week\n + Should only be used with most recent season''' + + if self.year < 2019: + raise Exception('Cant use free agents before 2019') + if not week: + week = self.current_week + + slot_filter = [] + if position and position in POSITION_MAP: + slot_filter = [POSITION_MAP[position]] + if position_id: + slot_filter.append(position_id) + + + params = { + 'view': 'kona_player_info', + 'scoringPeriodId': week, + } + filters = {"players":{"filterStatus":{"value":["FREEAGENT","WAIVERS"]},"filterSlotIds":{"value":slot_filter},"limit":size,"sortPercOwned":{"sortPriority":1,"sortAsc":False},"sortDraftRanks":{"sortPriority":100,"sortAsc":True,"value":"STANDARD"}}} + headers = {'x-fantasy-filter': json.dumps(filters)} + + data = self.espn_request.league_get(params=params, headers=headers) + players = data['players'] + + return [Player(player) for player in players] \ No newline at end of file diff --git a/espn_api/baseball/matchup.py b/espn_api/baseball/matchup.py new file mode 100644 index 00000000..24709951 --- /dev/null +++ b/espn_api/baseball/matchup.py @@ -0,0 +1,47 @@ +import pdb + +from .constant import STATS_MAP + +class Matchup(object): + '''Creates Matchup instance''' + def __init__(self, data): + self.home_team_live_score = None + self.away_team_live_score = None + self._fetch_matchup_info(data) + + def __repr__(self): + # TODO: use final score when that's available? + # writing this too early to see if data['home']['totalPoints'] is final score + # it might also be used for points leagues instead of category leagues + if not self.away_team_live_score: + return 'Matchup(%s, %s)' % (self.home_team, self.away_team, ) + else: + return 'Matchup(%s %s - %s %s)' % (self.home_team, + str(round(self.home_team_live_score, 1)), + str(round(self.away_team_live_score, 1)), + self.away_team) + + def _fetch_matchup_info(self, data): + '''Fetch info for matchup''' + self.home_team = data['home']['teamId'] + self.home_final_score = data['home']['totalPoints'] + self.away_team = data['away']['teamId'] + self.away_final_score = data['away']['totalPoints'] + self.winner = data['winner'] + self.home_team_cats = None + self.away_team_cats = None + + # if stats are available + if 'cumulativeScore' in data['home'].keys() and data['home']['cumulativeScore']['scoreByStat']: + + self.home_team_live_score = (data['home']['cumulativeScore']['wins'] + + data['home']['cumulativeScore']['ties']/2) + self.away_team_live_score = (data['away']['cumulativeScore']['wins'] + + data['away']['cumulativeScore']['ties']/2) + + self.home_team_cats = { STATS_MAP[i]: {'score': data['home']['cumulativeScore']['scoreByStat'][i]['score'], + 'result': data['home']['cumulativeScore']['scoreByStat'][i]['result']} for i in data['home']['cumulativeScore']['scoreByStat'].keys()} + + self.away_team_cats = { STATS_MAP[i]: {'score': data['away']['cumulativeScore']['scoreByStat'][i]['score'], + 'result': data['away']['cumulativeScore']['scoreByStat'][i]['result']} for i in data['away']['cumulativeScore']['scoreByStat'].keys()} + diff --git a/espn_api/baseball/player.py b/espn_api/baseball/player.py new file mode 100644 index 00000000..a6cb760c --- /dev/null +++ b/espn_api/baseball/player.py @@ -0,0 +1,24 @@ +from .constant import POSITION_MAP, PRO_TEAM_MAP, STATS_MAP +from .utils import json_parsing +import pdb + +class Player(object): + '''Player are part of team''' + def __init__(self, data): + self.name = json_parsing(data, 'fullName') + self.playerId = json_parsing(data, 'id') + self.position = POSITION_MAP[json_parsing(data, 'defaultPositionId') - 1] + self.lineupSlot = POSITION_MAP.get(data.get('lineupSlotId'), '') + self.eligibleSlots = [POSITION_MAP[pos] for pos in json_parsing(data, 'eligibleSlots')] + self.acquisitionType = json_parsing(data, 'acquisitionType') + self.proTeam = PRO_TEAM_MAP[json_parsing(data, 'proTeamId')] + self.injuryStatus = json_parsing(data, 'injuryStatus') + self.stats = {} + + # add available stats + player = data['playerPoolEntry']['player'] if 'playerPoolEntry' in data else data['player'] + self.injuryStatus = player.get('injuryStatus', self.injuryStatus) + self.injured = player.get('injured', False) + + def __repr__(self): + return 'Player(%s)' % (self.name, ) diff --git a/espn_api/baseball/team.py b/espn_api/baseball/team.py new file mode 100644 index 00000000..b64c81b2 --- /dev/null +++ b/espn_api/baseball/team.py @@ -0,0 +1,63 @@ +import pdb +from .player import Player +from .matchup import Matchup +from .constant import STATS_MAP + +class Team(object): + '''Teams are part of the league''' + def __init__(self, data, member, roster, schedule, year): + self.team_id = data['id'] + self.team_abbrev = data['abbrev'] + self.team_name = "%s %s" % (data['location'], data['nickname']) + self.division_id = data['divisionId'] + self.division_name = '' # set by caller + self.wins = data['record']['overall']['wins'] + self.losses = data['record']['overall']['losses'] + self.ties = data['record']['overall']['ties'] + self.owner = 'None' + self.logo_url = '' + self.stats = None + self.standing = data['playoffSeed'] + self.final_standing = data['rankCalculatedFinal'] + self.roster = [] + self.schedule = [] + + if 'valuesByStat' in data: + self.stats = {STATS_MAP[i]: j for i, j in data['valuesByStat'].items()} + if member: + self.owner = "%s %s" % (member['firstName'], + member['lastName']) + if 'logo' in data: + self.logo_url = data['logo'] + + self._fetch_roster(roster) + self._fetch_schedule(schedule) + + def __repr__(self): + return 'Team(%s)' % (self.team_name, ) + + + def _fetch_roster(self, data): + '''Fetch teams roster''' + self.roster.clear() + roster = data['entries'] + + for player in roster: + self.roster.append(Player(player)) + + + def _fetch_schedule(self, data): + '''Fetch schedule and scores for team''' + for match in data: + if 'away' in match.keys(): + if match['away']['teamId'] == self.team_id: + new_match = Matchup(match) + setattr(new_match, 'away_team', self) + self.schedule.append(new_match) + elif match['home']['teamId'] == self.team_id: + new_match = Matchup(match) + setattr(new_match, 'home_team', self) + self.schedule.append(new_match) + + + diff --git a/espn_api/baseball/utils.py b/espn_api/baseball/utils.py new file mode 100644 index 00000000..bd5690cf --- /dev/null +++ b/espn_api/baseball/utils.py @@ -0,0 +1,21 @@ +# Helper functions for json parsing and power rankings + +def json_parsing(obj, key): + """Recursively pull values of specified key from nested JSON.""" + arr = [] + + def extract(obj, arr, key): + """Return all matching values in an object.""" + if isinstance(obj, dict): + for k, v in obj.items(): + if isinstance(v, (dict)) or (isinstance(v, (list)) and v and isinstance(v[0], (list, dict))): + extract(v, arr, key) + elif k == key: + arr.append(v) + elif isinstance(obj, list): + for item in obj: + extract(item, arr, key) + return arr + + results = extract(obj, arr, key) + return results[0] if results else results diff --git a/tests/baseball/__init__.py b/tests/baseball/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/baseball/integration/__init__.py b/tests/baseball/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/baseball/integration/test_league.py b/tests/baseball/integration/test_league.py new file mode 100644 index 00000000..124e0697 --- /dev/null +++ b/tests/baseball/integration/test_league.py @@ -0,0 +1,23 @@ +from unittest import TestCase +from espn_api.baseball import League + +# Integration test to make sure ESPN's API didnt change +class LeagueTest(TestCase): + + def test_league_init(self): + league = League(81134470, 2021) + + self.assertEqual(len(league.teams), 8) + + # def test_league_scoreboard(self): + # league = League(81134470, 2021) + # scores = league.scoreboard() + + # self.assertEqual(scores[0].home_final_score, 4240.0) + # self.assertEqual(scores[0].away_final_score, 2965.0) + + def test_league_free_agents(self): + league = League(81134470, 2021) + free_agents = league.free_agents() + + self.assertNotEqual(len(free_agents), 0) \ No newline at end of file From c3e9e390ee49b7a5de4b26013756c4d7f4498f59 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 5 Apr 2021 16:56:02 -0700 Subject: [PATCH 2/2] update package version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 86534af8..1d076408 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name='espn_api', packages=find_packages(), - version='0.13.0', + version='0.14.0', author='Christian Wendt', description='ESPN API', install_requires=['requests>=2.0.0,<3.0.0', 'pandas>=0.11.0,<=0.25.3', 'numpy>=1.15,<=1.17.0'],