Project collects football data from external API.
from app.teams.get_team_statistics import GetTeamStatistics
class TeamStatisticsService:
LEAGUE = 'premier league'
COUNTRY = 'england'
def __init__(self, statistics_call=None):
"""
Store a request function used to make API calls.
"""
self._statistics_call = statistics_call or GetTeamStatistics()
self._cache = {}
def _get_response(self, team: str, season: int) -> dict:
"""Fetch and return the API response dict for a given team and season."""
key = (team, season)
if key not in self._cache:
self._cache[key] = self._statistics_call.get_team_statistics_response(
team=team, league=self.LEAGUE, country=self.COUNTRY, season=season)['response']
return self._cache[key]
def team_form(self, team: str, season: int) -> str:
return self._get_response(team, season)['form']
Testing the API response:
import pytest
from unittest.mock import patch
from app.teams.team_statistics_service import TeamStatisticsService
from app.teams.get_team_statistics import GetTeamStatistics
FAKE_TEAMS = {"response": [{"team": {"id": 33, "name": "Manchester United"}}]}
FAKE_SEASONS = {"response": [2022]}
# Prevents using the real API
@pytest.fixture(autouse=True)
def mock_external_calls():
with patch('app.teams.get_team_statistics.get_all_teams', return_value=FAKE_TEAMS), \
patch('app.teams.get_team_statistics.get_season_availability', return_value=FAKE_SEASONS):
yield
def fake_make_request(request_url: str):
return {
"response": [{
"league": {
"id": 39,
"name": "Premier League",
"country": "England",
"season": 2022,
},
"team": {
"id": 33,
"name": "Manchester United",
},
"form": "LLWWWWLWDWDWLWWWWWDLWDWWLDLWWWDWLLWWWW",
"fixtures": {
"played": {"home": 19, "away": 19, "total": 38},
"wins": {"home": 15, "away": 8, "total": 23},
"draws": {"home": 3, "away": 3, "total": 6},
"loses": {"home": 1, "away": 8, "total": 9},
},
"goals": {
"for": {
"total": {"home": 36, "away": 22, "total": 58},
"average": {"home": "1.9", "away": "1.2", "total": "1.5"},
"minute": {
"0-15": {"total": 6, "percentage": "10.53%"},
"16-30": {"total": 8, "percentage": "14.04%"},
"31-45": {"total": 10, "percentage": "17.54%"},
"46-60": {"total": 10, "percentage": "17.54%"},
"61-75": {"total": 9, "percentage": "15.79%"},
"76-90": {"total": 10, "percentage": "17.54%"},
"91-105": {"total": 4, "percentage": "7.02%"},
"106-120": {"total": None, "percentage": None},
},
"under_over": {
"0.5": {"over": 31, "under": 7},
"1.5": {"over": 20, "under": 18},
"2.5": {"over": 6, "under": 32},
"3.5": {"over": 1, "under": 37},
"4.5": {"over": 0, "under": 38},
},
},
"against": {
"total": {"home": 10, "away": 33, "total": 43},
"average": {"home": "0.5", "away": "1.7", "total": "1.1"},
"minute": {
"0-15": {"total": 6, "percentage": "13.64%"},
"16-30": {"total": 6, "percentage": "13.64%"},
"31-45": {"total": 7, "percentage": "15.91%"},
"46-60": {"total": 7, "percentage": "15.91%"},
"61-75": {"total": 7, "percentage": "15.91%"},
"76-90": {"total": 9, "percentage": "20.45%"},
"91-105": {"total": 2, "percentage": "4.55%"},
"106-120": {"total": None, "percentage": None},
},
"under_over": {
"0.5": {"over": 21, "under": 17},
"1.5": {"over": 9, "under": 29},
"2.5": {"over": 5, "under": 33},
"3.5": {"over": 3, "under": 35},
"4.5": {"over": 2, "under": 36},
},
},
},
"biggest": {
"streak": {"wins": 5, "draws": 1, "loses": 2},
"wins": {"home": "4-1", "away": "0-2"},
"loses": {"home": "1-2", "away": "7-0"},
"goals": {
"for": {"home": 4, "away": 3},
"against": {"home": 2, "away": 7},
},
},
"clean_sheet": {"home": 11, "away": 6, "total": 17},
"failed_to_score": {"home": 2, "away": 5, "total": 7},
"penalty": {
"scored": {"total": 3, "percentage": "100.00%"},
"missed": {"total": 0, "percentage": "0%"},
"total": 3,
},
"cards": {
"yellow": {
"0-15": {"total": 4, "percentage": "5.13%"},
"16-30": {"total": 8, "percentage": "10.26%"},
"31-45": {"total": 8, "percentage": "10.26%"},
"46-60": {"total": 13, "percentage": "16.67%"},
"61-75": {"total": 16, "percentage": "20.51%"},
"76-90": {"total": 18, "percentage": "23.08%"},
"91-105": {"total": 11, "percentage": "14.10%"},
"106-120": {"total": None, "percentage": None},
},
"red": {
"0-15": {"total": None, "percentage": None},
"16-30": {"total": None, "percentage": None},
"31-45": {"total": 1, "percentage": "50.00%"},
"46-60": {"total": None, "percentage": None},
"61-75": {"total": 1, "percentage": "50.00%"},
"76-90": {"total": None, "percentage": None},
"91-105": {"total": None, "percentage": None},
"106-120": {"total": None, "percentage": None},
},
},
}]
}
TEAM = 'manchester united'
SEASON = 2022
class TestTeamStatisticsService:
def setup_method(self):
mock_get_team_stats = GetTeamStatistics(request_fn=fake_make_request)
self.service = TeamStatisticsService(
statistics_call=mock_get_team_stats)
# ------------------------------------------------------------------ #
# Form #
# ------------------------------------------------------------------ #
def test_team_form(self):
assert self.service.team_form(
TEAM, SEASON) == "LLWWWWLWDWDWLWWWWWDLWDWWLDLWWWDWLLWWWW"
Questions/comments:
I'm not sure whether self._statistics_call = statistics_call or GetTeamStatistics() in the test setup is the cleanest way?
I aimed to not use real API calls in tests.
Are the fixtures the best way to setup the test with the mock data?
The cacheing prevents repeated calls if i already have the data, is there a clearer/better way to do this?