"""
footballdata.datasets
~~~~~~~~~~~~~~~~~~~~~
This module implements the data structures used to represent the data
returned from the API.
:copyright: (c) 2018 Tony Joseph
:license: BSD 3-Clause
"""
from dateutil.parser import parse as datetime_parse
from .utils import fetch_data_from_api, clean_object
[docs]class Competition(FootballDataObject):
"""Class to represent a competition"""
def __init__(self, **kwargs):
"""Initialises new competition object"""
# These values are set by FootballDataObject constructor using values in kwargs
self.id = ''
self.caption = ''
self.current_match_day = ''
self.last_updated = ''
self.league = ''
self.number_of_games = ''
self.number_of_teams = ''
self.year = ''
self.links = {}
super().__init__(**kwargs)
# Set competition attributes
self.fixtures_endpoint = self.links['fixtures']['href']
self.league_table_endpoint = self.links['leagueTable']['href']
self.teams_endpoint = self.links['teams']['href']
self.base_endpoint = self.links['self']['href']
# :TODO: These empty lists should be replaced with empty data set objects
self.__teams = []
self.__fixtures = []
self.__league_table = []
def __repr__(self):
name = self.caption or 'Unknown'
return "Competition <{!r}>".format(name)
[docs] def get_teams(self, force_update=False):
"""Fetches all teams in a competition
:param force_update: Boolean, overrides cached results if True
:return: DataSet of Team objects
"""
if force_update or not self.__teams:
self.__teams = DataSet(klass=Team, endpoint=self.teams_endpoint, api_key=self.api_key)
return self.__teams
[docs] def get_fixtures(self, force_update=False):
"""Fetches all fixtures in a competition
:param force_update: Boolean, overrides cached results if True
:return: DataSet of Fixture objects
"""
if force_update or not self.__fixtures:
self.__fixtures = DataSet(klass=Fixture, endpoint=self.fixtures_endpoint, api_key=self.api_key)
return self.__fixtures
[docs] def get_league_table(self, force_update=False):
"""Fetches standing of all teams in competition
:param force_update: Boolean, overrides cached results if True
:return: DataSet of Standing objects
"""
if force_update or not self.__league_table:
self.__league_table = DataSet(klass=Standing, endpoint=self.league_table_endpoint, api_key=self.api_key)
return self.__league_table
[docs]class Fixture(FootballDataObject):
"""Class to represent a fixture"""
def __init__(self, **kwargs):
"""Initialises new Fixture object"""
# These values are set by FootballDataObject constructor using values in kwargs
self.date = ''
self.away_team_name = ''
self.home_team_name = ''
self.match_day = ''
self.odds = ''
self.result = ''
self.status = ''
super().__init__(**kwargs)
# Convert date to datetime object
if hasattr(self, 'date'):
self.date = datetime_parse(self.date)
def __repr__(self):
home_team = self.home_team_name or 'Unknown'
away_team = self.away_team_name or 'Unknown'
date = self.date or 'Unknown'
return "Fixture <{!r} v {!r} on {!r}>".format(home_team, away_team, date)
[docs]class Team(FootballDataObject):
"""Class to represent a team"""
def __init__(self, **kwargs):
# These values are set by FootballDataObject constructor using values in kwargs
self.code = ''
self.crest_url = ''
self.name = ''
self.short_name = ''
self.squad_market_value = ''
self.links = {}
super().__init__(**kwargs)
# Set extra attributes
self.fixtures_endpoint = self.links['fixtures']['href']
self.players_endpoint = self.links['players']['href']
self.base_endpoint = self.links['self']['href']
# :TODO: These empty lists should be replaced with empty data set objects
self.__fixtures = []
self.__players = []
def __repr__(self):
name = self.name or 'Unknown'
return "Team <{!r}>".format(name)
[docs] def get_fixtures(self, force_update=False):
"""Fetches all fixtures for a team
:param force_update: Boolean, overrides cached results if True
:return: DataSet of Fixture objects
"""
if force_update or not self.__fixtures:
self.__fixtures = DataSet(klass=Fixture, endpoint=self.fixtures_endpoint, api_key=self.api_key)
return self.__fixtures
[docs] def get_players(self, force_update=False):
"""Fetches all players in a team
:param force_update: Boolean, overrides cached results if True
:return: DataSet of Player objects
"""
if force_update or not self.__players:
self.__players = DataSet(klass=Player, endpoint=self.players_endpoint, api_key=self.api_key)
return self.__players
[docs]class Standing(FootballDataObject):
"""Class to represent a team's standing in a competition"""
def __init__(self, **kwargs):
# These values are set by FootballDataObject constructor using values in kwargs
self.team_name = ''
self.crest_uri = ''
self.played_games = ''
self.wins = ''
self.draws = ''
self.losses = ''
self.home = ''
self.away = ''
self.points = ''
self.position = ''
self.goals = ''
self.goals_against = ''
self.goal_difference = ''
super().__init__(**kwargs)
def __repr__(self):
team_name = self.team_name or 'Unknown'
return "Standing <{!r}>".format(team_name)
[docs]class Player(FootballDataObject):
"""Class to represent a player"""
def __init__(self, **kwargs):
"""Initialises Player object"""
# These values are set by FootballDataObject constructor using values in kwargs
self.name = ''
self.nationality = ''
self.position = ''
self.contract_until = ''
self.date_of_birth = ''
self.jersey_number = ''
self.market_value = ''
super().__init__(**kwargs)
# Convert contract_until and date_of_birth to datetime objects
if hasattr(self, 'contract_until') and self.contract_until:
self.contract_until = datetime_parse(self.contract_until)
if hasattr(self, 'date_of_birth') and self.date_of_birth:
self.date_of_birth = datetime_parse(self.date_of_birth)
def __repr__(self):
name = self.name or 'Unknown'
return "Player <{!r}>".format(name)
[docs]class DataSet:
"""Class to represent a sequence of football data objects"""
def __init__(self, klass, endpoint='', api_key='', options=None, data_list=None):
"""Initialises FootballDataObject sequence
:param klass: type, Class of objects in data set. Should be either FootballDataObject or its subclass
:param endpoint: str, API endpoint to fetch data
:param api_key: str, API key
:param options: dict, Additional arguments to sent with API call
:param data_list: iterable containing klass objects
"""
self.__klass = klass
self.__endpoint = endpoint
self.__api_key = api_key
self.__options = options if options else {}
# Set data_list as data_set if available
if data_list:
# Type check all elements in iterable
if not all(isinstance(item, klass) for item in data_list):
raise TypeError("All items should be an instance of {}".format(klass))
self.__data_set = list(data_list)
else:
self.__data_set = []
def __repr__(self):
return "DataSet <{!r}>".format(self.__klass.__name__)
def __create_data_set_item(self, cleaned_data):
"""Creates a single football data object
:param cleaned_data: Dict with proper key value pairs
:return: FootballDataObject object
"""
data_set_item = self.__klass(**cleaned_data)
# Build base endpoint if id is available
if hasattr(data_set_item, 'id'):
data_set_item.base_endpoint = "{}{}".format(self.__endpoint, data_set_item.id)
else:
data_set_item.base_endpoint = self.__endpoint
data_set_item.api_key = self.__api_key
return data_set_item
def __load_data_set(self):
"""Loads data from football-data.org in not already loaded"""
if not self.__data_set and self.__endpoint:
data_list = fetch_data_from_api(endpoint=self.__endpoint, api_key=self.__api_key, options=self.__options)
if data_list:
# Handles inconsistent API structures
if self.__klass == Team:
# Actual team list is in dict with key teams
data_list = data_list['teams']
elif self.__klass == Fixture:
# Actual fixture list is in dict with key fixtures
data_list = data_list['fixtures']
elif self.__klass == Standing:
# Handle differences in league and cup standing
if 'standing' in data_list:
# Competition is a league
data_list = data_list['standing']
else:
# No league table available
return
elif self.__klass == Player:
data_list = data_list['players']
cleaned_data_list = map(clean_object, data_list)
self.__data_set = list(map(self.__create_data_set_item, cleaned_data_list))
else:
# Data list fetching failed due to some reason
self.__data_set = []
def __iter__(self):
self.__load_data_set()
for data in self.__data_set:
yield data
def __getitem__(self, key):
self.__load_data_set()
if isinstance(key, int):
# If key is an integer, return item at index
try:
return self.__data_set[key]
except IndexError:
raise IndexError('Index out of range')
elif isinstance(key, slice):
return DataSet(klass=self.__klass, data_list=self.__data_set[key])
raise TypeError('Key must be an integer or slice object')
def __len__(self):
self.__load_data_set()
return len(self.__data_set)
def __bool__(self):
self.__load_data_set()
return bool(self.__data_set)