Skip to content

Commit

Permalink
Merge pull request #601 from alan-turing-institute/develop
Browse files Browse the repository at this point in the history
[develop] Next AIrsenal release
  • Loading branch information
jack89roberts committed Aug 7, 2023
2 parents 24d5169 + 3e60987 commit a7bfb2a
Show file tree
Hide file tree
Showing 39 changed files with 178,795 additions and 9,237 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9]
python-version: [3.9]

steps:
- uses: actions/checkout@v3
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.3.0
rev: v4.4.0
hooks:
- id: check-yaml
- id: check-toml
Expand All @@ -13,11 +13,11 @@ repos:
hooks:
- id: isort
- repo: https://github.com/ambv/black
rev: 22.3.0
rev: 23.7.0
hooks:
- id: black
language_version: python3.9
- repo: https://github.com/pycqa/flake8
rev: 3.9.2
rev: 6.1.0
hooks:
- id: flake8
4 changes: 2 additions & 2 deletions airsenal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
___init__.py for airsenal
"""

import pkg_resources
from importlib.metadata import version

# AIrsenal package version.
__version__ = pkg_resources.get_distribution(__name__).version
__version__ = version(__name__)
1,631 changes: 719 additions & 912 deletions airsenal/data/absences_1516.csv

Large diffs are not rendered by default.

1,649 changes: 746 additions & 903 deletions airsenal/data/absences_1617.csv

Large diffs are not rendered by default.

1,448 changes: 659 additions & 789 deletions airsenal/data/absences_1718.csv

Large diffs are not rendered by default.

2,601 changes: 1,227 additions & 1,374 deletions airsenal/data/absences_1819.csv

Large diffs are not rendered by default.

2,325 changes: 1,047 additions & 1,278 deletions airsenal/data/absences_1920.csv

Large diffs are not rendered by default.

2,277 changes: 1,032 additions & 1,245 deletions airsenal/data/absences_2021.csv

Large diffs are not rendered by default.

1,802 changes: 800 additions & 1,002 deletions airsenal/data/absences_2122.csv

Large diffs are not rendered by default.

867 changes: 567 additions & 300 deletions airsenal/data/absences_2223.csv

Large diffs are not rendered by default.

166,619 changes: 166,619 additions & 0 deletions airsenal/data/minutes_estimation_challenge.csv

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion airsenal/framework/api_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def combine_player_info(player_id, dbsession=DBSESSION):
tag = get_latest_prediction_tag(dbsession=dbsession)
predicted_points = get_predicted_points_for_player(p, tag, dbsession=dbsession)
info_dict["predictions"] = predicted_points
except (RuntimeError):
except RuntimeError:
pass
return info_dict

Expand Down
143 changes: 90 additions & 53 deletions airsenal/framework/data_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class FPLDataFetcher(object):
def __init__(self, fpl_team_id=None, rsession=None):
self.rsession = rsession or requests.session()
self.logged_in = False
self.login_failed = False
self.current_summary_data = None
self.current_event_data = None
self.current_player_data = None
Expand Down Expand Up @@ -103,12 +104,17 @@ def get_fpl_credentials(self):
save_env("FPL_LOGIN", self.FPL_LOGIN)
save_env("FPL_PASSWORD", self.FPL_PASSWORD)

def login(self):
def login(self, attempts=3):
"""
only needed for accessing mini-league data, or team info for current gw.
"""
if self.logged_in:
return
if self.login_failed:
raise RuntimeError(
"Attempted to use a function requiring login, but login previously "
"failed."
)
if (
(not self.FPL_LOGIN)
or (not self.FPL_PASSWORD)
Expand All @@ -130,8 +136,10 @@ def login(self):
if do_login.lower() == "y":
self.get_fpl_credentials()
else:
return

self.login_failed = True
raise RuntimeError(
"Requested logging into the FPL API but no credentials provided."
)
headers = {
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 5.1; PRO 5 Build/LMY47D)"
}
Expand All @@ -141,12 +149,24 @@ def login(self):
"app": "plfpl-web",
"redirect_uri": self.FPL_LOGIN_REDIRECT_URL,
}
response = self.rsession.post(self.FPL_LOGIN_URL, data=data, headers=headers)
if response.status_code != 200:
print(f"Error loging in: {response.content}")
else:
print("Logged in successfully")
self.logged_in = True
tried = 0
while tried < attempts:
print(f"Login attempt {tried+1}/{attempts}...", end=" ")
response = self.rsession.post(
self.FPL_LOGIN_URL, data=data, headers=headers
)
if response.status_code == 200:
print("Logged in successfully")
self.logged_in = True
return
print("Failed")
tried += 1
time.sleep(1)
try:
response.raise_for_status()
except requests.HTTPError as e:
self.login_failed = True
raise requests.HTTPError(f"Error logging in to FPL API: {e}")

def get_current_squad_data(self, fpl_team_id=None):
"""
Expand Down Expand Up @@ -193,6 +213,7 @@ def get_current_bank(self, fpl_team_id=None):
def get_available_chips(self, fpl_team_id=None):
"""
Returns a list of chips that are available to be played in upcoming gameweek.
Requires login
"""
squad_data = self.get_current_squad_data(fpl_team_id)
return [
Expand Down Expand Up @@ -265,8 +286,8 @@ def get_fpl_transfer_data(self, fpl_team_id=None):
self._get_request(
url,
(
"Unable to access FPL "
f"transfer history API for team_id {fpl_team_id}"
"Unable to access FPL transfer history API for "
f"team_id {fpl_team_id}"
),
)
)
Expand Down Expand Up @@ -360,25 +381,10 @@ def get_gameweek_data_for_player(self, player_api_id, gameweek=None):
if (not gameweek) or (
gameweek not in self.player_gameweek_data[player_api_id].keys()
):
got_data = False
n_tries = 0
player_detail = {}
while (not got_data) and n_tries < 3:
try:
player_detail = self._get_request(
self.FPL_DETAIL_URL.format(player_api_id),
f"Error retrieving data for player {player_api_id}",
)
if player_detail is None:
return []
got_data = True
except requests.exceptions.ConnectionError:
print(f"connection error, retrying {n_tries}")
time.sleep(1)
n_tries += 1
if not player_detail:
print(f"Unable to get player_detail data for {player_api_id}")
return []
player_detail = self._get_request(
self.FPL_DETAIL_URL.format(player_api_id),
f"Error retrieving data for player {player_api_id}",
)
for game in player_detail["history"]:
gw = game["round"]
if gw not in self.player_gameweek_data[player_api_id].keys():
Expand Down Expand Up @@ -416,39 +422,40 @@ def get_lineup(self):
"""
Retrieve up to date lineup from api
"""

self.login()

team_url = self.FPL_MYTEAM_URL.format(self.FPL_TEAM_ID)

return self._get_request(team_url)

def post_lineup(self, payload):
"""
Set the lineup for a specific team
"""

self.login()

payload = json.dumps({"chip": None, "picks": payload})
headers = {
"Content-Type": "application/json; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": "https://fantasy.premierleague.com/a/team/my",
}

team_url = self.FPL_MYTEAM_URL.format(self.FPL_TEAM_ID)

resp = self.rsession.post(team_url, data=payload, headers=headers)
try:
resp.raise_for_status()
except requests.HTTPError as e:
raise requests.HTTPError(
f"{e}\nLineup changes not made due to the error above! Make the "
"changes manually on the web-site if needed."
)
if resp.status_code == 200:
print("SUCCESS....lineup made!")
else:
print("Lineup changes not made due to unknown error")
print(f"Response status code: {resp.status_code}")
print(f"Response text: {resp.text}")
return
raise Exception(
f"Unexpected error in post_lineup: "
f"code={resp.status_code}, content={resp.content}"
)

def post_transfers(self, transfer_payload):

self.login()

# adapted from https://github.com/amosbastian/fpl/blob/master/fpl/utils.py
Expand All @@ -464,17 +471,47 @@ def post_transfers(self, transfer_payload):
transfer_url, data=json.dumps(transfer_payload), headers=headers
)
if "non_form_errors" in resp:
raise Exception(resp["non_form_errors"])
elif resp.status_code == 200:
raise requests.RequestException(
f"{resp['non_form_errors']}\nMaking transfers failed due to the "
"error above! Make the changes manually on the web-site if needed."
)
try:
resp.raise_for_status()
except requests.HTTPError as e:
raise requests.HTTPError(
f"{e}\nMaking transfers failed due to the error above! Make the "
"changes manually on the web-site if needed."
)
if resp.status_code == 200:
print("SUCCESS....transfers made!")
else:
print("Transfers unsuccessful due to unknown error")
print(f"Response status code: {resp.status_code}")
print(f"Response text: {resp.text}")
return
raise Exception(
f"Unexpected error in post_transfers: "
f"code={resp.status_code}, content={resp.content}"
)

def _get_request(self, url, err_msg="Unable to access FPL API"):
r = self.rsession.get(url)
if r.status_code != 200:
print(err_msg)
return None
return json.loads(r.content.decode("utf-8"))
def _get_request(self, url, err_msg="Unable to access FPL API", attempts=3):
tries = 0
while tries < attempts:
try:
r = self.rsession.get(url)
break
except requests.exceptions.ConnectionError as e:
tries += 1
if tries == attempts:
raise requests.exceptions.ConnectionError(
f"{err_msg}: Failed to connect to FPL API when requesting {url}"
) from e
time.sleep(1)

if r.status_code == 200:
return json.loads(r.content.decode("utf-8"))

try:
r.raise_for_status()
except requests.HTTPError as e:
raise requests.HTTPError(f"{err_msg}: {e}") from e
raise Exception(
f"Unexpected error in _get_request to {url}: "
f"code={r.status_code}, content={r.content}"
)
47 changes: 32 additions & 15 deletions airsenal/framework/optimization_utils.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""
functions to optimize the transfers for N weeks ahead
"""
import warnings
from copy import deepcopy
from datetime import datetime
from typing import Optional

import requests

from airsenal.framework.schema import (
Fixture,
PlayerPrediction,
Expand Down Expand Up @@ -91,33 +94,47 @@ def calc_free_transfers(num_transfers, prev_free_transfers):


def get_starting_squad(
season=CURRENT_SEASON, fpl_team_id=None, use_api=False, apifetcher=None
next_gw=NEXT_GAMEWEEK,
season=CURRENT_SEASON,
fpl_team_id=None,
use_api=False,
apifetcher=None,
):
"""
use the transactions table in the db, or the API if requested
"""
if use_api:
if season != CURRENT_SEASON:
raise RuntimeError("Can only use API for current season")
raise RuntimeError("Can only use API for current season and gameweek")
if season == CURRENT_SEASON and next_gw != NEXT_GAMEWEEK:
raise RuntimeError("Can only use API for current season and gameweek")
if not fpl_team_id:
raise RuntimeError(
"Please specify fpl_team_id to get current squad from API"
)
players_prices = get_current_squad_from_api(fpl_team_id, apifetcher=apifetcher)
s = Squad(season=CURRENT_SEASON)
for pp in players_prices:
s.add_player(
pp[0],
price=pp[1],
gameweek=NEXT_GAMEWEEK - 1,
check_budget=False,
check_team=False,
try:
players_prices = get_current_squad_from_api(
fpl_team_id, apifetcher=apifetcher
)
s = Squad(season=CURRENT_SEASON)
for pp in players_prices:
s.add_player(
pp[0],
price=pp[1],
gameweek=next_gw - 1,
check_budget=False,
check_team=False,
)
s.budget = get_bank(fpl_team_id, season=CURRENT_SEASON)
return s
except requests.exceptions.RequestException as e:
warnings.warn(
f"Failed to get current squad from API:\n{e}\nUsing DB instead, which "
"may be out of date."
)
s.budget = get_bank(fpl_team_id, season=CURRENT_SEASON)
return s

# otherwise, we use the Transaction table in the DB
s = get_squad_from_transactions(NEXT_GAMEWEEK - 1, season, fpl_team_id)
return s
return get_squad_from_transactions(next_gw - 1, season, fpl_team_id)


def get_squad_from_transactions(gameweek, season=CURRENT_SEASON, fpl_team_id=None):
Expand Down
6 changes: 3 additions & 3 deletions airsenal/framework/player_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def fit(
progress_bar=True,
**(mcmc_kwargs or {}),
)
rng_key, rng_key_predict = random.split(random.PRNGKey(44))
rng_key, rng_key_predict = random.split(random.PRNGKey(random_state))
mcmc.run(
rng_key,
data["nplayer"],
Expand Down Expand Up @@ -207,7 +207,7 @@ def get_probs(self):
def get_probs_for_player(self, player_id):
try:
index = list(self.player_ids).index(player_id)
except (ValueError):
except ValueError:
raise RuntimeError(f"Unknown player_id {player_id}")
prob_score = float(self.samples["probs"][:, index, 0].mean())
prob_assist = float(self.samples["probs"][:, index, 1].mean())
Expand Down Expand Up @@ -273,6 +273,6 @@ def get_probs(self) -> Dict[str, np.ndarray]:
def get_probs_for_player(self, player_id: int) -> np.ndarray:
try:
index = list(self.player_ids).index(player_id)
except (ValueError):
except ValueError:
raise RuntimeError(f"Unknown player_id {player_id}")
return self.mean_probabilities[index, :]
Loading

0 comments on commit a7bfb2a

Please sign in to comment.