Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions espn_api/base_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ def __init__(self, data):
self._raw_schedule_settings = data.get('scheduleSettings', {})
self.faab = data['acquisitionSettings']['isUsingAcquisitionBudget']
self.acquisition_budget = data.get('acquisitionSettings', {}).get('acquisitionBudget', 0)
self.acquisition_limit = data.get('acquisitionSettings', {}).get('acquisitionLimit')
self.matchup_acquisition_limit = data.get('acquisitionSettings', {}).get('matchupAcquisitionLimit')
self.matchup_limit_per_scoring_period = data.get('acquisitionSettings', {}).get('matchupLimitPerScoringPeriod')
self.minimum_bid = data.get('acquisitionSettings', {}).get('minimumBid', 0)
self.waiver_process_days = list(data.get('acquisitionSettings', {}).get('waiverProcessDays', []))
self.waiver_process_hour = data.get('acquisitionSettings', {}).get('waiverProcessHour')
self.trade_revision_hours = data.get('tradeSettings', {}).get('revisionHours')
divisions = data.get('scheduleSettings', {}).get('divisions', [])
for division in divisions: self.division_map[division.get('id', 0)] = division.get('name')

Expand Down
25 changes: 15 additions & 10 deletions espn_api/baseball/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
__all__ = ['League',
'Team',
'Player',
'Matchup',
]

from .league import League
from .team import Team
from .player import Player
from .matchup import Matchup
__all__ = ['League',
'Team',
'Player',
'Matchup',
'Transaction',
'TransactionItem',
'Settings',
]

from .league import League
from .team import Team
from .player import Player
from .matchup import Matchup
from .transaction import Transaction, TransactionItem
from .settings import Settings
69 changes: 68 additions & 1 deletion espn_api/baseball/box_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def _process_team(self, team_data, is_home_team):

def _get_team_data(self, team, data, pro_schedule, week, year):
if team not in data:
return (0, 0, -1, [])
return (0, 0, -1, []) # -1 projected score indicates no projection available (bye week / missing)

team_id = data[team]['teamId']
team_projected = -1
Expand All @@ -95,3 +95,70 @@ def _get_team_data(self, team, data, pro_schedule, week, year):
team_lineup = [BoxPlayer(player, pro_schedule, week, year) for player in team_roster]

return (team_id, team_score, team_projected, team_lineup)


class RotoBoxScore(BoxScore):
'''Boxscore for rotisserie (ROTO) leagues.

In roto there is no home/away matchup — all teams accumulate stats and are
ranked against each other in each category. One RotoBoxScore is returned
per matchup period, containing every team's cumulative category stats and
their league-wide rank in each category.

Inherits from BoxScore so that isinstance(x, BoxScore) holds for all four
baseball box-score shapes, but winner/home_team/away_team are always None
since roto has no head-to-head matchup.

Attributes:
matchup_period (int): the matchup period this snapshot covers
teams (list[dict]): one entry per team, each containing:
- team : team_id (int) initially; replaced with Team object
by League.box_scores()
- total_points (float): cumulative roto points (sum of category ranks)
- stats : dict mapping stat name → {'score': float, 'rank': float}
- lineup : list[BoxPlayer] for the current scoring period
'''
def __init__(self, data, pro_schedule, year, scoring_period=0):
# Skip BoxScore.__init__ — roto data has no winner/home/away keys.
# Set the inherited attributes to None so callers can still access them.
self.winner = None
self.home_team = None
self.away_team = None

self.matchup_period = data.get('matchupPeriodId')
self.teams = []

for team_data in data.get('teams', []):
team_id = team_data['teamId']
live = team_data.get('totalPointsLive')
total = round(live if live is not None else team_data.get('totalPoints', 0), 2)

stats = {}
for stat_id_str, stat_dict in team_data.get('cumulativeScore', {}).get('scoreByStat', {}).items():
stat_name = STATS_MAP.get(int(stat_id_str), f'stat_{stat_id_str}')
score = stat_dict['score']
# ESPN sends the literal string 'Infinity' for rate stats (e.g. WHIP,
# ERA) when the denominator is zero — coerce to float('inf') so
# callers can do arithmetic/comparisons without type-switching.
if score == 'Infinity':
score = float('inf')
stats[stat_name] = {'score': score, 'rank': stat_dict['rank']}

entries = team_data.get('rosterForCurrentScoringPeriod', {}).get('entries', [])
lineup = [BoxPlayer(p, pro_schedule, scoring_period, year) for p in entries]

self.teams.append({
'team': team_id,
'total_points': total,
'stats': stats,
'lineup': lineup,
})

def _process_team(self, team_data, is_home_team):
# Roto has no home/away concept — this abstract method is required by
# the BoxScore base class but is never called (RotoBoxScore overrides
# __init__ entirely).
pass

def __repr__(self):
return f'Roto Box Score(period:{self.matchup_period})'
30 changes: 30 additions & 0 deletions espn_api/baseball/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,31 @@
99: 'STARTER',
}

STAT_SPLIT_MAP = {
0: 'season',
1: 'last_7',
2: 'last_15',
3: 'last_30',
5: 'box_score',
}

# Stat IDs that apply only to pitchers
PITCHER_ONLY_STATS = {
32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 62, 63, 65, 66, 76, 77, 82, 83
}

# Stat IDs that apply only to batters (excludes shared fielding stats and games played)
BATTER_ONLY_STATS = {
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, 21,
23, 24, 25, 26, 27, 28, 29, 31, 74, 75
}

# Eligibility-slot names used to classify a player as a pitcher or batter
# when filtering stats by role (e.g. dropping batter-only stats from a pitcher).
PITCHER_POSITIONS = {'SP', 'RP', 'P'}
BATTER_POSITIONS = {'C', '1B', '2B', '3B', 'SS', 'OF', 'LF', 'CF', 'RF', 'DH', 'UTIL', '2B/SS', '1B/3B'}

ACTIVITY_MAP = {
178: 'FA ADDED',
180: 'WAIVER ADDED',
Expand All @@ -200,3 +225,8 @@
'TRADED': 244
}

TRANSACTION_TYPES = {
'DRAFT', 'TRADE_ACCEPT', 'WAIVER', 'TRADE_VETO', 'FUTURE_ROSTER',
'ROSTER', 'RETRO_ROSTER', 'TRADE_PROPOSAL', 'TRADE_UPHOLD',
'FREEAGENT', 'TRADE_DECLINE', 'WAIVER_ERROR', 'TRADE_ERROR'
}
103 changes: 90 additions & 13 deletions espn_api/baseball/league.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
import time
import json
import math
from typing import List, Tuple, Union
from typing import List, Set, Tuple, Union

from ..base_league import BaseLeague
from .team import Team
from .player import Player
from .matchup import Matchup
from .box_score import BoxScore, H2HCategoryBoxScore, H2HPointsBoxScore
from .box_score import BoxScore, H2HCategoryBoxScore, H2HPointsBoxScore, RotoBoxScore
from .activity import Activity
from .constant import POSITION_MAP, ACTIVITY_MAP
from .constant import POSITION_MAP, ACTIVITY_MAP, TRANSACTION_TYPES
from .settings import Settings
from .transaction import Transaction

class League(BaseLeague):
'''Creates a League instance for Public/Private ESPN league'''

ScoreTypes = {'H2H_CATEGORY': H2HCategoryBoxScore, 'H2H_POINTS': H2HPointsBoxScore}
ScoreTypes = {'H2H_CATEGORY': H2HCategoryBoxScore, 'H2H_POINTS': H2HPointsBoxScore, 'ROTO': RotoBoxScore}

def __init__(self, league_id: int, year: int, espn_s2=None, swid=None, fetch_league=True, debug=False):
super().__init__(league_id=league_id, year=year, sport='mlb', espn_s2=espn_s2, swid=swid, debug=debug)
Expand All @@ -38,7 +40,7 @@ def fetch_league(self):
super()._fetch_draft()

def _fetch_league(self):
data = super()._fetch_league()
data = super()._fetch_league(SettingsClass=Settings)
self._fetch_players()
return data

Expand Down Expand Up @@ -81,6 +83,30 @@ def scoreboard(self, matchupPeriod: int = None) -> List[Matchup]:

return matchups

def transactions(self, types: Set[str] = None, scoring_period: int = None) -> List[Transaction]:
'''Returns a list of transactions for a given scoring period'''
if types is None:
types = TRANSACTION_TYPES
for t in types:
if t not in TRANSACTION_TYPES:
raise ValueError(f'Invalid transaction type: {t}. Valid types: {TRANSACTION_TYPES}')

if scoring_period is None:
scoring_period = self.scoringPeriodId

params = {
'view': 'mTransactions2',
'scoringPeriodId': scoring_period,
}
filters = {'transactions': {'filterType': {'value': list(types)}}}
headers = {'x-fantasy-filter': json.dumps(filters)}
data = self.espn_request.league_get(params=params, headers=headers)

return [
Transaction(t, self.player_map, self.get_team_data)
for t in data.get('transactions', [])
]

def recent_activity(self, size: int = 25, msg_type: str = None, offset: int = 0) -> List[Activity]:
'''Returns a list of recent league activities (Add, Drop, Trade)'''
if self.year < 2019:
Expand Down Expand Up @@ -129,8 +155,16 @@ def free_agents(self, week: int=None, size: int=50, position: str=None, position

return [Player(player, self.year) for player in players]

def box_scores(self, matchup_period: int = None, scoring_period: int = None) -> List[Union[BoxScore, H2HCategoryBoxScore]]:
'''Returns list of box score for a given matchup or scoring period'''
def box_scores(self, matchup_period: int = None, scoring_period: int = None) -> List[BoxScore]:
'''Returns list of box score for a given matchup or scoring period.

The concrete type depends on the league's scoring_type:
- H2H_CATEGORY → H2HCategoryBoxScore (home/away with category results)
- H2H_POINTS → H2HPointsBoxScore (home/away with point totals)
- ROTO → RotoBoxScore (no home/away — callers iterate
the .teams list instead)
All three inherit from BoxScore.
'''
if self.year < 2019:
raise Exception('Cant use box score before 2019')

Expand All @@ -155,10 +189,53 @@ def box_scores(self, matchup_period: int = None, scoring_period: int = None) ->
schedule = data['schedule']
box_data = [self._box_score_class(matchup, pro_schedule, self.year, scoring_id) for matchup in schedule]

for team in self.teams:
for matchup in box_data:
if matchup.home_team == team.team_id:
matchup.home_team = team
elif matchup.away_team == team.team_id:
matchup.away_team = team
team_map = {t.team_id: t for t in self.teams}
for matchup in box_data:
if self.scoring_type == 'ROTO':
for entry in matchup.teams:
entry['team'] = team_map.get(entry['team'], entry['team'])
else:
if matchup.home_team in team_map:
matchup.home_team = team_map[matchup.home_team]
if matchup.away_team in team_map:
matchup.away_team = team_map[matchup.away_team]
return box_data

def player_info(self, name: str = None, playerId: Union[int, list] = None) -> Union[Player, List[Player]]:
'''Returns Player class if name or playerId found'''
if name and name in self.player_map:
playerId = self.player_map[name]
if playerId is None:
return None
if not isinstance(playerId, list):
playerId = [playerId]

split_filters = ["0{}{}".format(i, self.year) for i in range(1, 4)]
data = self.espn_request.get_player_card(playerId, self.finalScoringPeriod, additional_filters=split_filters)
if len(data['players']) == 1:
return Player(data['players'][0], self.year)
if len(data['players']) > 1:
return [Player(player, self.year) for player in data['players']]

def refresh(self):
'''Gets latest league data without re-fetching all players'''
data = super()._fetch_league(SettingsClass=Settings)
self.scoring_type = data['settings']['scoringSettings']['scoringType']
self._fetch_teams(data)
self._box_score_class = self._set_scoring_class(self.scoring_type)

def load_roster_week(self, week: int) -> None:
'''Sets Teams Roster for a Certain Week'''
params = {
'view': 'mRoster',
'scoringPeriodId': week
}
data = self.espn_request.league_get(params=params)

team_roster = {}
for team in data['teams']:
team_roster[team['id']] = team['roster']

for team in self.teams:
roster = team_roster[team.team_id]
team._fetch_roster(roster, self.year)
2 changes: 0 additions & 2 deletions espn_api/baseball/matchup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import pdb

from .constant import STATS_MAP

class Matchup(object):
Expand Down
Loading
Loading