Skip to content

Commit

Permalink
Merge pull request #397 from alan-turing-institute/develop
Browse files Browse the repository at this point in the history
Recent Development
  • Loading branch information
jack89roberts authored Sep 17, 2021
2 parents 266b5e8 + f602d40 commit beb4966
Show file tree
Hide file tree
Showing 22 changed files with 3,798 additions and 207 deletions.
6 changes: 2 additions & 4 deletions airsenal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,8 @@
import os
import tempfile

# AIrsenal package version. When merging changes to master:
# - increment 2nd digit for new features
# - increment 3rd digit for bug fixes
__version__ = "1.0.0"
# AIrsenal package version.
__version__ = "1.1.0"

# Cross-platform temporary directory
TMPDIR = "/tmp/" if os.name == "posix" else tempfile.gettempdir()
101 changes: 91 additions & 10 deletions airsenal/framework/data_fetcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,26 @@ class FPLDataFetcher(object):

def __init__(self, fpl_team_id=None, rsession=None):
self.rsession = rsession if rsession else requests.session()
self.logged_in = False
self.current_summary_data = None
self.current_event_data = None
self.current_player_data = None
self.current_team_data = None
self.current_squad_data = None
self.player_gameweek_data = {}
self.fpl_team_history_data = None
# transfer history data is a dict, keyed by fpl_team_id
self.fpl_transfer_history_data = {}
self.fpl_league_data = None
self.fpl_team_data = {} # players in squad, by gameweek
self.fixture_data = None
for ID in ["FPL_LEAGUE_ID", "FPL_TEAM_ID", "FPL_LOGIN", "FPL_PASSWORD"]:
for ID in [
"FPL_LEAGUE_ID",
"FPL_TEAM_ID",
"FPL_LOGIN",
"FPL_PASSWORD",
"DISCORD_WEBHOOK",
]:
if ID in os.environ.keys():
self.__setattr__(ID, os.environ[ID])
elif os.path.exists(
Expand Down Expand Up @@ -63,15 +71,19 @@ def __init__(self, fpl_team_id=None, rsession=None):
self.FPL_LOGIN_REDIRECT_URL = "https://fantasy.premierleague.com/a/login"
self.FPL_MYTEAM_URL = API_HOME + "/my-team/{}/"

# login, if desired

# self.login()

def get_fpl_credentials(self):
"""
If we didn't have FPL_LOGIN and FPL_PASSWORD available as files in
airsenal/data or as environment variables, prompt the user for them.
"""
print(
"""
Accessing FPL mini-league data requires the login (email address) and
password for your FPL account.
Accessing the most up-to-date data on your squad, or automatic transfers,
requires the login (email address) and password for your FPL account.
"""
)
self.FPL_LOGIN = input("Please enter FPL login: ")
Expand All @@ -90,42 +102,111 @@ def get_fpl_credentials(self):
login_file.write(self.FPL_LOGIN)
with open(os.path.join(data_loc, "FPL_PASSWORD"), "w") as passwd_file:
passwd_file.write(self.FPL_PASSWORD)
print(
"""
Wrote files {} and {}.
You may need to do 'pip install .' for these to be picked up.
""".format(
login_file, passwd_file
)
)

def login(self):
"""
only needed for accessing mini-league data, or team info for current gw.
"""

if self.logged_in:
return
if (
(not self.FPL_LOGIN)
or (not self.FPL_PASSWORD)
or (self.FPL_LOGIN == "MISSING_ID")
or (self.FPL_PASSWORD == "MISSING_ID")
):
self.get_fpl_credentials()
do_login = ""
while do_login.lower() not in ["y", "n"]:
do_login = input(
(
"\nWould you like to login to the FPL API?"
"\nThis is not necessary for most AIrsenal actions, "
"\nbut may improve accuracy of player sell values,"
"\nand free transfers for your team, and will also "
"\nenable AIrsenal to make transfers for you through "
"\nthe API. (y/n): "
)
)
if do_login.lower() == "y":
self.get_fpl_credentials()
else:
return

headers = {
"login": self.FPL_LOGIN,
"password": self.FPL_PASSWORD,
"app": "plfpl-web",
"redirect_uri": self.FPL_LOGIN_REDIRECT_URL,
}
self.rsession.post(self.FPL_LOGIN_URL, data=headers)
response = self.rsession.post(self.FPL_LOGIN_URL, data=headers)
if response.status_code != 200:
print(f"Error loging in: {response.content}")
else:
print("Logged in successfully")
self.logged_in = True

def get_current_squad_data(self, fpl_team_id=None):
"""
Requires login. Return the current "picks".
Requires login. Return the current squad data, including
"picks", bank, and free transfers.
"""
if self.current_squad_data:
return self.current_squad_data
if fpl_team_id:
team_id = fpl_team_id
elif self.FPL_TEAM_ID and not self.FPL_TEAM_ID == "MISSING_ID":
elif self.FPL_TEAM_ID and self.FPL_TEAM_ID != "MISSING_ID":
team_id = self.FPL_TEAM_ID
else:
raise RuntimeError("Please specify FPL team ID")
self.login()
url = self.FPL_MYTEAM_URL.format(team_id)
data = self._get_request(url)
return data["picks"]
self.current_squad_data = self._get_request(url)
return self.current_squad_data

def get_current_picks(self, fpl_team_id=None):
"""
Returns the players picked for the upcoming gameweek, including
purchase and selling prices, and whether they are subs or not.
Requires login
"""
squad_data = self.get_current_squad_data(fpl_team_id)
return squad_data["picks"]

def get_num_free_transfers(self, fpl_team_id=None):
"""
Returns the number of free transfers for the upcoming gameweek.
Requires login
"""
squad_data = self.get_current_squad_data(fpl_team_id)
return squad_data["transfers"]["limit"]

def get_current_bank(self, fpl_team_id=None):
"""
Returns the remaining bank (in 0.1M) for the upcoming gameweek.
Requires login
"""
squad_data = self.get_current_squad_data(fpl_team_id)
return squad_data["transfers"]["bank"]

def get_available_chips(self, fpl_team_id=None):
"""
Returns a list of chips that are available to be played in upcoming gameweek.
"""
squad_data = self.get_current_squad_data(fpl_team_id)
chip_list = [
chip["name"]
for chip in squad_data["chips"]
if chip["status_for_entry"] == "available"
]
return chip_list

def get_current_summary_data(self):
"""
Expand Down
19 changes: 19 additions & 0 deletions airsenal/framework/multiprocessing_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,25 @@

from multiprocessing.queues import Queue
import multiprocessing
import os


def set_multiprocessing_start_method(num_thread=2):
"""To fix change of default behaviour in multiprocessing on Python 3.8 and later
on MacOS. Python 3.8 and later start processess using spawn by default, see:
https://docs.python.org/3.8/library/multiprocessing.html#contexts-and-start-methods
Note that this should be called at most once, ideally protecteed within
if __name__ == "__main__"
Parameters
----------
num_thread : int, optional
Only changem ultiprocessing start method if num_thread > 1, by default 2
"""
if num_thread is not None and num_thread > 1 and os.name == "posix":
multiprocessing.set_start_method("fork")


# The following implementation of custom MyQueue to avoid NotImplementedError
# when calling queue.qsize() in MacOS X comes almost entirely from this github
Expand Down
12 changes: 5 additions & 7 deletions airsenal/framework/optimization_pygmo.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,10 @@ def fitness(self, player_ids):
- weghted sum of gameweek points otherwise
"""
# Make squad from player IDs
squad = Squad(budget=self.budget)
squad = Squad(budget=self.budget, season=self.season)
for idx in player_ids:
add_ok = squad.add_player(
self.players[int(idx)].player_id,
season=self.season,
gameweek=self.start_gw,
)
if not add_ok:
Expand Down Expand Up @@ -351,18 +350,17 @@ def make_new_squad_pygmo(
print("Best score:", -pop.champion_f[0], "pts")

# construct optimal squad
squad = Squad(budget=opt_squad.budget)
squad = Squad(budget=opt_squad.budget, season=season)
for idx in pop.champion_x:
if verbose > 0:
print(
opt_squad.players[int(idx)].position(CURRENT_SEASON),
opt_squad.players[int(idx)].position(season),
opt_squad.players[int(idx)].name,
opt_squad.players[int(idx)].team(CURRENT_SEASON, 1),
opt_squad.players[int(idx)].price(CURRENT_SEASON, 1) / 10,
opt_squad.players[int(idx)].team(season, 1),
opt_squad.players[int(idx)].price(season, 1) / 10,
)
squad.add_player(
opt_squad.players[int(idx)].player_id,
season=opt_squad.season,
gameweek=opt_squad.start_gw,
)

Expand Down
12 changes: 5 additions & 7 deletions airsenal/framework/optimization_squad.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,14 +87,14 @@ def make_new_squad_iter(
# this was passed as a tuple (func, increment, pid)
update_func_and_args[0](update_func_and_args[1], update_func_and_args[2])
predicted_points = {}
t = Squad(budget)
t = Squad(budget, season=season)
# first iteration - fill up from the front
for pos in positions:
predicted_points[pos] = get_predicted_points(
gameweek=gw_range, position=pos, tag=tag, season=season
)
for pp in predicted_points[pos]:
t.add_player(pp[0], season=season, gameweek=transfer_gw)
t.add_player(pp[0], gameweek=transfer_gw)
if t.num_position[pos] == TOTAL_PER_POSITION[pos]:
break

Expand All @@ -105,9 +105,7 @@ def make_new_squad_iter(
# same position
player_to_remove = t.players[random.randint(0, len(t.players) - 1)]
remove_cost = player_to_remove.purchase_price
t.remove_player(
player_to_remove.player_id, season=season, gameweek=transfer_gw
)
t.remove_player(player_to_remove.player_id, gameweek=transfer_gw)
excluded_player_ids.append(player_to_remove.player_id)
for pp in predicted_points[player_to_remove.position]:
if (
Expand All @@ -117,7 +115,7 @@ def make_new_squad_iter(
if cp.purchase_price >= remove_cost:
continue
else:
t.add_player(pp[0], season=season, gameweek=transfer_gw)
t.add_player(pp[0], gameweek=transfer_gw)
# now try again to fill up the rest of the squad
for pos in positions:
num_missing = TOTAL_PER_POSITION[pos] - t.num_position[pos]
Expand All @@ -126,7 +124,7 @@ def make_new_squad_iter(
for pp in predicted_points[pos]:
if pp[0] in excluded_player_ids:
continue
t.add_player(pp[0], season=season, gameweek=transfer_gw)
t.add_player(pp[0], gameweek=transfer_gw)
if t.num_position[pos] == TOTAL_PER_POSITION[pos]:
break
# we have a complete squad
Expand Down
30 changes: 9 additions & 21 deletions airsenal/framework/optimization_transfers.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,11 @@ def make_optimum_single_transfer(
position = p_out.position
if verbose:
print("Removing player {}".format(p_out.player_id))
new_squad.remove_player(p_out.player_id, season=season, gameweek=transfer_gw)
new_squad.remove_player(p_out.player_id, gameweek=transfer_gw)
for p_in in ordered_player_lists[position]:
if p_in[0].player_id == p_out.player_id:
continue # no point in adding the same player back in
added_ok = new_squad.add_player(
p_in[0], season=season, gameweek=transfer_gw
)
added_ok = new_squad.add_player(p_in[0], gameweek=transfer_gw)
if added_ok:
if verbose:
print("Added player {}".format(p_in[0].name))
Expand Down Expand Up @@ -127,9 +125,7 @@ def make_optimum_double_transfer(
pout_1 = squad.players[i]

new_squad_remove_1 = fastcopy(squad)
new_squad_remove_1.remove_player(
pout_1.player_id, season=season, gameweek=transfer_gw
)
new_squad_remove_1.remove_player(pout_1.player_id, gameweek=transfer_gw)
for j in range(i + 1, len(squad.players)):
if update_func_and_args:
# call function to update progress bar.
Expand All @@ -140,9 +136,7 @@ def make_optimum_double_transfer(

pout_2 = squad.players[j]
new_squad_remove_2 = fastcopy(new_squad_remove_1)
new_squad_remove_2.remove_player(
pout_2.player_id, season=season, gameweek=transfer_gw
)
new_squad_remove_2.remove_player(pout_2.player_id, gameweek=transfer_gw)
if verbose:
print("Removing players {} {}".format(i, j))
# what positions do we need to fill?
Expand All @@ -153,9 +147,7 @@ def make_optimum_double_transfer(
if pin_1[0].player_id in [pout_1.player_id, pout_2.player_id]:
continue # no point in adding same player back in
new_squad_add_1 = fastcopy(new_squad_remove_2)
added_1_ok = new_squad_add_1.add_player(
pin_1[0], season=season, gameweek=transfer_gw
)
added_1_ok = new_squad_add_1.add_player(pin_1[0], gameweek=transfer_gw)
if not added_1_ok:
continue
for pin_2 in ordered_player_lists[positions_needed[1]]:
Expand All @@ -167,7 +159,7 @@ def make_optimum_double_transfer(
):
continue # no point in adding same player back in
added_2_ok = new_squad_add_2.add_player(
pin_2[0], season=season, gameweek=transfer_gw
pin_2[0], gameweek=transfer_gw
)
if added_2_ok:
# calculate the score
Expand Down Expand Up @@ -248,9 +240,7 @@ def make_random_transfers(
for p in players_to_remove:
positions_needed.append(squad.players[p].position)
removed_players.append(squad.players[p].player_id)
new_squad.remove_player(
removed_players[-1], season=season, gameweek=transfer_gw
)
new_squad.remove_player(removed_players[-1], gameweek=transfer_gw)
predicted_points = {
pos: get_predicted_points(position=pos, gameweek=gw_range, tag=tag)
for pos in set(positions_needed)
Expand All @@ -265,9 +255,7 @@ def make_random_transfers(
for pos in positions_needed:
index = int(random.triangular(0, len(predicted_points[pos]), 0))
pid_to_add = predicted_points[pos][index][0]
added_ok = new_squad.add_player(
pid_to_add, season=season, gameweek=transfer_gw
)
added_ok = new_squad.add_player(pid_to_add, gameweek=transfer_gw)
if added_ok:
added_players.append(pid_to_add)
complete_squad = new_squad.is_complete()
Expand All @@ -280,7 +268,7 @@ def make_random_transfers(
# take those players out again.
for ap in added_players:
removed_ok = new_squad.remove_player(
ap.player_id, season=season, gameweek=transfer_gw
ap.player_id, gameweek=transfer_gw
)
if not removed_ok:
print("Problem removing {}".format(ap.name))
Expand Down
Loading

0 comments on commit beb4966

Please sign in to comment.