Skip to content

Commit

Permalink
Merge pull request #609 from alan-turing-institute/608-logging-in-issue
Browse files Browse the repository at this point in the history
Fix #608 logging in issue
  • Loading branch information
jack89roberts committed Aug 6, 2023
2 parents 27090fe + d47e207 commit bd7e3e8
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 174 deletions.
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}"
)
37 changes: 24 additions & 13 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 @@ -109,21 +112,29 @@ def get_starting_squad(
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_gw - 1,
check_budget=False,
check_team=False,
try:
players_prices = get_current_squad_from_api(
fpl_team_id, apifetcher=apifetcher
)
s.budget = get_bank(fpl_team_id, season=CURRENT_SEASON)
return s
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."
)

# otherwise, we use the Transaction table in the DB
s = get_squad_from_transactions(next_gw - 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
47 changes: 25 additions & 22 deletions airsenal/framework/squad.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Contains a set of players.
Is able to check that it obeys all constraints.
"""
import warnings
from operator import itemgetter

import numpy as np
Expand Down Expand Up @@ -175,37 +176,41 @@ def get_sell_price_for_player(
if isinstance(player, int):
player = self.get_player_from_id(player) # get CandidatePlayer from squad
player_id = player.player_id

price_now = None
if use_api and self.season == CURRENT_SEASON and gameweek >= NEXT_GAMEWEEK:
player_db = get_player(player_id)
api_id = player_db.fpl_api_id
# first try getting the actual sale price from a logged in API
try:
return apifetcher.get_current_picks()[api_id]["selling_price"]
except Exception as e:
warnings.warn(
f"Failed to login to get actual sale price for {player} from API:\n"
f"{e}.\nWill estimate it based on the players current price instead"
)
# if not logged in, just get current price from API
try:
# first try getting the price for the player from the API
player_db = get_player(player_id)
api_id = player_db.fpl_api_id
if (
apifetcher.logged_in
and apifetcher.FPL_TEAM_ID
and apifetcher.FPL_TEAM_ID != "MISSING_ID"
):
return apifetcher.get_current_picks()[api_id]["selling_price"]
# if not logged in, just get current price from API
price_now = apifetcher.get_player_summary_data()[api_id]["now_cost"]
except Exception:
pass
except Exception as e:
warnings.warn(
f"Failed to to get current price of {player} from API:\n"
f"{e}.\nWill attempt to use latest price in DB instead."
)

# retrieve how much we originally bought the player for from db
price_bought = player.purchase_price

# get player's current price from db if the API wasn't used
if not price_now:
player_db = get_player(player_id, dbsession=dbsession)

if player_db:
price_now = player_db.price(self.season, gameweek)

# if all else fails just use the purchase price as the sale price for the player
if not price_now:
# if all else fails just use the purchase price as the sale
# price for this player.
print(
"Using purchase price as sale price for",
player.player_id,
player,
warnings.warn(
f"Using purchase price as sale price for {player.player_id}, {player}"
)
price_now = price_bought

Expand Down Expand Up @@ -267,7 +272,7 @@ def optimize_subs(self, gameweek, tag):
try:
points_prediction = p.predicted_points[tag][gameweek]

except (KeyError):
except KeyError:
# player does not have a game in this gameweek
points_prediction = 0
player_dict[p.position].append((p, points_prediction))
Expand Down Expand Up @@ -357,7 +362,6 @@ def total_points_for_starting_11(self, gameweek, tag, triple_captain=False):
def total_points_for_subs(
self, gameweek, tag, sub_weights={"GK": 1, "Outfield": (1, 1, 1)}
):

outfield_subs = [
p for p in self.players if (not p.is_starting) and (p.position != "GK")
]
Expand All @@ -375,7 +379,6 @@ def total_points_for_subs(
return total

def optimize_lineup(self, gameweek, tag):

if not self.is_complete():
raise RuntimeError("Squad is incomplete")

Expand Down
Loading

0 comments on commit bd7e3e8

Please sign in to comment.